Compare commits

..
2 Commits
Author SHA1 Message Date
FooIbar 75bec4314e Release 1.8.9.0-gms
Releases / Build (push) Has been cancelled
2023-08-11 16:03:44 +08:00
FooIbar b767c041b9 Load Cronet from Google Play services 2023-08-11 15:56:21 +08:00
75 changed files with 1262 additions and 760 deletions

No files matched your search

+4 -1
View File
@@ -135,7 +135,10 @@ body:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
+5 -1
View File
@@ -1 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
@@ -0,0 +1,94 @@
name: 功能请求 / Feature request
description: 提出一个功能建议 / Suggest an idea
labels:
- "enhancement"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交功能建议前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
label: 需求 / Requirement
description: |
在此处描述您的需求。这通常会是一个您想要的功能
Ddescribe what you need here.
placeholder: |
我需要 XXX 功能 ...
I want ABC feature ...
validations:
required: true
- type: textarea
id: impl
attributes:
label: 建议实现 / Suggested implements
description: |
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
placeholder: |
建议在 XX 处添加 XX ...
I recommend add ABC feature to DEF...
图片(如果有)/ Photos (if exists)
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 1.6.9.13
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
placeholder: 26f05a0
validations:
required: true
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Verify steps
description: |
请确认您已经遵守所有必选项
Please ensure you have obtained all needed options
options:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
@@ -25,7 +25,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
+2 -2
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
@@ -20,7 +20,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
@@ -23,7 +23,7 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
-4
View File
@@ -1,7 +1,3 @@
# Maintenance Mode
The project is in maintenance mode, will only receive bugfixes.
<p align="right">
<strong>English</strong>
<span> | </span>
+20 -17
View File
@@ -21,7 +21,7 @@ plugins {
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10636728"
ndkVersion = "26.0.10404224"
splits {
abi {
@@ -64,7 +64,7 @@ android {
minSdk = 28
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.6"
versionName = "1.8.9.0-gms"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -159,7 +159,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
kotlinCompilerExtensionVersion = "1.5.1"
}
namespace = "com.hippo.ehviewer"
@@ -167,11 +167,11 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.8.0-beta01")
implementation("androidx.activity:activity-compose:1.8.0-alpha06")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.6.0")
implementation("androidx.collection:collection-ktx:1.3.0-rc01")
implementation("androidx.collection:collection-ktx:1.3.0-beta01")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.05.00"))
@@ -181,29 +181,29 @@ dependencies {
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.ui:ui-util")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.12.0-rc01")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha04")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.7.2"
val nav_version = "2.7.0"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging
val paging = "3.2.1"
val paging = "3.2.0"
implementation("androidx.paging:paging-compose:$paging")
implementation("androidx.paging:paging-runtime-ktx:$paging")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.6.0-beta01"
val room_version = "2.6.0-alpha03"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
@@ -213,10 +213,10 @@ dependencies {
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
// https://github.com/google/accompanist/releases
val accompanist_version = "0.32.0"
val accompanist_version = "0.31.6-rc"
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
implementation("com.google.android.material:material:1.11.0-alpha02")
implementation("com.google.android.material:material:1.11.0-alpha01")
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
@@ -238,7 +238,7 @@ dependencies {
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation(platform("io.arrow-kt:arrow-stack:1.2.1"))
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
implementation("io.arrow-kt:arrow-fx-coroutines")
// https://coil-kt.github.io/coil/changelog/
@@ -246,11 +246,14 @@ dependencies {
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
val ktor = "2.3.4"
val ktor = "2.3.3"
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
val serialization = "1.6.0"
implementation("org.chromium.net:cronet-api:113.5672.61")
implementation("com.google.android.gms:play-services-cronet:18.0.1")
val serialization = "1.5.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
@@ -296,6 +299,6 @@ tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
dependsOn("cargoBuild")
// fix mergeDebugJniLibFolders UP-TO-DATE
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
inputs.dir(buildDir.resolve("rustJniLibs/android"))
}
}
-1
View File
@@ -17,7 +17,6 @@
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
@@ -11,6 +11,7 @@ import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
@@ -25,6 +25,7 @@ import androidx.lifecycle.coroutineScope
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
@@ -77,6 +78,7 @@ class EhApplication : Application(), ImageLoaderFactory {
handler?.uncaughtException(t, e)
}
super.onCreate()
CronetProviderInstaller.installProvider(appCtx)
System.loadLibrary("ehviewer")
System.loadLibrary("ehviewer_rust")
ReadableTime.initialize(this)
@@ -30,16 +30,16 @@ object Settings : DataStorePreferences(null) {
var downloadQuery by stringOrNullPref("image_query", null)
var downloadFragment by stringOrNullPref("image_fragment", null)
var archivePasswds by stringSetOrNullPref("archive_passwds")
var downloadDelay by intPref("download_delay_2", 0)
var gallerySite by intPref("gallery_site_2", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intPref("download_thread_2", 3)
var preloadImage by intPref("preload_image_2", 5)
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
var listMode by intPref("list_mode_2", 0)
var detailSize by intPref("detail_size_2", 0)
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 downloadDelay by intFromStrPref("download_delay", 0)
var gallerySite by intFromStrPref("gallery_site", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intFromStrPref("download_thread", 3)
var preloadImage by intFromStrPref("preload_image", 5)
var theme by intFromStrPref("theme", -1).observed { updateWhenThemeChanges() }
var listMode by intFromStrPref("list_mode", 0)
var detailSize by intFromStrPref("detail_size", 0)
var thumbResolution by intFromStrPref("thumb_resolution", 0)
var readCacheSize by intFromStrPref("read_cache_size", 640)
var launchPage by intFromStrPref("launch_page", 0)
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() }
@@ -60,9 +60,9 @@ object Settings : DataStorePreferences(null) {
var removeImageFiles by boolPref("include_pic", true)
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)
var downloadOriginImage by boolPref("download_origin_image", false)
var enableQuic by boolPref("enable_quic", true)
var thumbSizeDp by intPref("thumb_size_", 120)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
@@ -95,6 +95,13 @@ object Settings : DataStorePreferences(null) {
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
}
private fun intFromStrPref(key: String, defValue: Int) = object : Delegate<Int> {
override val flowGetter: () -> Flow<Unit>
private var _value by stringPref(key, defValue.toString()).also { flowGetter = { it.changesFlow() } }
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = _value.toIntOrNull() ?: defValue
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Int) { _value = value.toString() }
}
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
@@ -120,8 +120,8 @@ private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwab
} else if ("<" !in body) {
throw EhException(body)
} else {
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e, body)
throw EhException(appCtx.getString(R.string.error_parse_error), e)
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
throw EhException(appCtx.getString(R.string.error_parse_error))
}
}
@@ -126,18 +126,12 @@ object EhUtils {
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
return if (Settings.harmonizeCategoryColor) {
harmonizeWithRole(context, primary)
} else {
primary
}
return harmonizeWithRole(context, primary)
}
@Composable
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
val categoryTextColor = Color(0xffe6e0e9)
fun signOut() {
EhCookieStore.signOut()
Settings.avatar = null
@@ -70,8 +70,8 @@ data class ListUrlBuilder(
mPrev = index?.takeUnless { isNext }
}
fun setJumpTo(jumpTo: Int) {
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
}
var keyword: String?
@@ -16,7 +16,16 @@
package com.hippo.ehviewer.client.exception
class ParseException : EhException {
constructor(detailMessage: String?) : super(detailMessage)
val body: String
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
constructor(detailMessage: String?, body: String) : super(detailMessage) {
this.body = body
}
constructor(detailMessage: String?, body: String, cause: Throwable?) : super(
detailMessage,
cause,
) {
this.body = body
}
}
@@ -12,7 +12,7 @@ object FavoritesParser {
}
val catArray = arrayOfNulls<String>(10)
val countArray = parseFav(body, catArray)
if (countArray.isEmpty()) throw ParseException("Parse favorites error")
if (countArray.isEmpty()) throw ParseException("Parse favorites error", body)
val result = GalleryListParser.parse(body)
return Result(catArray.requireNoNulls(), countArray, result)
}
@@ -114,9 +114,9 @@ object GalleryDetailParser {
gd.token = groupValues[2]
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
gd.apiKey = groupValues[4]
} ?: throw ParseException("Can't parse gallery detail")
} ?: throw ParseException("Can't parse gallery detail", body)
if (gd.gid == -1L) {
throw ParseException("Can't parse gallery detail")
throw ParseException("Can't parse gallery detail", body)
}
PATTERN_TORRENT.find(body)?.run {
gd.torrentUrl = groupValues[1].trim().unescapeXml()
@@ -214,7 +214,7 @@ object GalleryDetailParser {
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
throw ParseException("Can't parse gallery detail")
throw ParseException("Can't parse gallery detail", body)
}
// newer version
@@ -450,7 +450,7 @@ object GalleryDetailParser {
*/
fun parsePreviewPages(body: String): Int {
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error")
?: throw ParseException("Parse preview page count error", body)
}
/**
@@ -458,7 +458,7 @@ object GalleryDetailParser {
*/
fun parsePages(body: String): Int {
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error")
?: throw ParseException("Parse pages error", body)
}
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
@@ -1,12 +1,9 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
object GalleryListParser {
suspend fun parse(body: String) = runCatching {
@@ -18,21 +15,13 @@ object GalleryListParser {
it.generateSLang()
}
}
}.getOrElse {
if (body.contains("<p>You do not have any watched tags")) {
throw EhException(R.string.gallery_list_empty_hit_subscription)
}
if (body.contains("No hits found</p>")) {
throw EhException(R.string.gallery_list_empty_hit)
}
throw ParseException("Can't parse gallery list", it)
}
}.getOrDefault(GalleryListResult())
}
class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
val prev: String? = null,
val next: String? = null,
val galleryInfoList: ArrayList<BaseGalleryInfo> = arrayListOf(),
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -33,7 +33,7 @@ object GalleryMultiPageViewerPTokenParser {
imagelist.parseAs<List<Item>>().map(Item::token)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse pToken from MPV error", it)
throw ParseException("Parse pToken from MPV error", body, it)
}
}
@@ -27,7 +27,7 @@ object GalleryPageApiParser {
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String): Result {
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
@@ -41,7 +41,7 @@ object GalleryPageApiParser {
if (!imageUrl.isNullOrEmpty()) {
return Result(imageUrl, skipHathKey, originImageUrl)
} else {
throw ParseException("Parse image url and skip hath key error")
throw ParseException("Parse image url and skip hath key error", body)
}
}
@@ -56,7 +56,7 @@ object GalleryPageParser {
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey)
} else {
throw ParseException("Parse image url and show error")
throw ParseException("Parse image url and show error", body)
}
}
@@ -24,7 +24,7 @@ object HomeParser {
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
return Funds(fundsGP, fundsC)
}
throw ParseException("Parse funds error")
throw ParseException("Parse funds error", body)
}
@Parcelize
@@ -46,7 +46,7 @@ object ProfileParser {
Result(displayName, avatar)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse forums error")
throw ParseException("Parse forums error", body)
}
}
@@ -35,7 +35,7 @@ object SignInParser {
if (m.find()) {
throw EhException(m.group(1) ?: m.group(2))
} else {
throw ParseException("Can't parse sign in")
throw ParseException("Can't parse sign in", body)
}
}
}
@@ -1,12 +1,9 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
object TorrentParser {
fun parse(body: ByteBuffer) = runCatching {
parseTorrent(body)
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
fun parse(body: ByteBuffer) = parseTorrent(body)
}
class Torrent(
@@ -1,12 +1,9 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.coil
import android.net.Uri
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresExtension
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.DataSource
@@ -30,7 +27,7 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option
val diskCache = requireNotNull(imageLoader.diskCache)
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
val success = cronetRequest(data) {
setCacheDisabled(true)
disableCache()
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
@@ -1,13 +1,5 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.cronet
import android.net.http.HttpEngine
import android.net.http.HttpException
import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
@@ -20,6 +12,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.Path.Companion.toOkioPath
import org.chromium.net.CronetEngine
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import splitties.init.appCtx
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
@@ -32,23 +28,26 @@ import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(CHROME_USER_AGENT)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
}.build()
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
// TODO: Rewrite this to use android.net.http.HttpEngine and make it Android 14 only when released
class CronetRequest {
lateinit var consumer: (ByteBuffer) -> Unit
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
lateinit var request: UrlRequest
lateinit var onError: (Throwable) -> Unit
lateinit var readerCont: Continuation<Unit>
val callback = object : UrlRequest.Callback {
val callback = object : UrlRequest.Callback() {
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
@@ -67,19 +66,16 @@ class CronetRequest {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
onError(e)
}
override fun onCanceled(req: UrlRequest, info: UrlResponseInfo?) {
// No-op
}
}
}
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
@@ -25,7 +25,7 @@ import java.time.Instant
),
],
)
data class DownloadEntity(
class DownloadEntity(
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long,
@@ -44,23 +44,21 @@ data class DownloadEntity(
@ColumnInfo(name = "POSITION")
override var position: Int = 0,
@Ignore
override var speed: Long = 0,
@Ignore
override var remaining: Long = 0,
@Ignore
override var finished: Int = 0,
@Ignore
override var downloaded: Int = 0,
@Ignore
override var total: Int = 0,
) : AbstractDownloadInfo {
constructor() : this(0)
@Ignore
override var speed: Long = 0
@Ignore
override var remaining: Long = 0
@Ignore
override var finished = 0
@Ignore
override var downloaded = 0
@Ignore
override var total = 0
}
data class DownloadInfo(
@@ -71,7 +71,7 @@ object DownloadManager : OnSpiderListener {
// Store download info wait to start
private val mWaitList = LinkedList<DownloadInfo>()
private val mSpeedReminder = SpeedReminder()
private var mDownloadInfoListener: DownloadInfoListener? = null
private val mDownloadInfoListeners = mutableListOf<DownloadInfoListener>()
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
private var mDownloadListener: DownloadListener? = null
private var mCurrentTask: DownloadInfo? = null
@@ -125,6 +125,14 @@ object DownloadManager : OnSpiderListener {
return info?.state ?: DownloadInfo.STATE_INVALID
}
fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.add(downloadInfoListener)
}
fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.remove(downloadInfoListener)
}
fun setDownloadListener(listener: DownloadListener?) {
mDownloadListener = listener
}
@@ -158,7 +166,12 @@ object DownloadManager : OnSpiderListener {
mDownloadListener!!.onStart(info)
}
// Notify state update
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
}
}
@@ -179,7 +192,12 @@ object DownloadManager : OnSpiderListener {
// Update in DB
EhDB.putDownloadInfo(info)
// Notify state update
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
// Make sure download is running
ensureDownload()
}
@@ -209,7 +227,9 @@ object DownloadManager : OnSpiderListener {
EhDB.putDownloadInfo(info)
// Notify
mDownloadInfoListener?.onUpdate(info)
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
// Make sure download is running
ensureDownload()
@@ -239,7 +259,9 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
// Ensure download
ensureDownload()
}
@@ -249,23 +271,33 @@ object DownloadManager : OnSpiderListener {
private val _stateFlow = callbackFlow {
val listener = object : DownloadInfoListener {
override fun onUpdate(info: DownloadInfo) {
trySend(info.gid)
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
trySend(info)
}
override fun onUpdateAll() {
trySend(0)
// No-op
}
override fun onReload() {
// No-op
}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
}
}
mDownloadInfoListener = listener
addDownloadInfoListener(listener)
awaitClose {
mDownloadInfoListener = null
removeDownloadInfoListener(listener)
}
}.shareIn(callbackFlowScope, SharingStarted.Eagerly)
fun stateFlow(gid: Long) = _stateFlow.filter { it == gid }
fun stateFlow() = _stateFlow
fun stateFlow(gid: Long) = _stateFlow.filter { it.gid == gid }
suspend fun startAllDownload() {
var update = false
@@ -283,7 +315,9 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
// Ensure download
ensureDownload()
}
@@ -319,7 +353,9 @@ object DownloadManager : OnSpiderListener {
// Notify
if (notify) {
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onReload()
}
}
}
@@ -350,7 +386,9 @@ object DownloadManager : OnSpiderListener {
// Notify
withUIContext {
mDownloadInfoListener?.onUpdate(info)
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
}
}
@@ -358,7 +396,12 @@ object DownloadManager : OnSpiderListener {
val info = stopDownloadInternal(gid)
if (info != null) {
// Update listener
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
// Ensure download
ensureDownload()
}
@@ -368,7 +411,12 @@ object DownloadManager : OnSpiderListener {
val info = stopCurrentDownloadInternal()
if (info != null) {
// Update listener
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
// Ensure download
ensureDownload()
}
@@ -378,7 +426,9 @@ object DownloadManager : OnSpiderListener {
stopRangeDownloadInternal(gidList)
// Update listener
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
// Ensure download
ensureDownload()
@@ -397,7 +447,9 @@ object DownloadManager : OnSpiderListener {
stopCurrentDownloadInternal()
// Notify mDownloadInfoListener
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
}
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
@@ -418,7 +470,9 @@ object DownloadManager : OnSpiderListener {
if (index >= 0) {
list.remove(info)
// Update listener
mDownloadInfoListener?.onUpdate(info)
for (l in mDownloadInfoListeners) {
l.onRemove(info, list, index)
}
}
}
@@ -441,13 +495,15 @@ object DownloadManager : OnSpiderListener {
allInfoList.removeAll(list.toSet())
// Update listener
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onReload()
}
// Ensure download
ensureDownload()
}
private fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
val info = allInfoList.removeAt(fromPosition)
allInfoList.add(toPosition, info)
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
@@ -459,15 +515,12 @@ object DownloadManager : OnSpiderListener {
return list
}
fun moveDownload(fromItem: DownloadInfo, toItem: DownloadInfo): List<DownloadInfo> {
val fromPosition = allInfoList.indexOf(fromItem)
val toPosition = allInfoList.indexOf(toItem)
return if (fromPosition != -1 && toPosition != -1) {
moveDownload(fromPosition, toPosition)
} else {
emptyList()
fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) =
getInfoListForLabel(label)!!.let {
val absFromPosition = allInfoList.indexOf(it[fromPosition])
val absToPosition = allInfoList.indexOf(it[toPosition])
moveDownload(absFromPosition, absToPosition)
}
}
suspend fun resetAllReadingProgress() = coroutineScope {
allInfoList.forEach { info ->
@@ -605,7 +658,9 @@ object DownloadManager : OnSpiderListener {
// Save to DB
EhDB.putDownloadInfo(info)
}
mDownloadInfoListener?.onUpdateAll()
for (l in mDownloadInfoListeners) {
l.onReload()
}
}
suspend fun addLabel(label: String?) {
@@ -755,15 +810,30 @@ object DownloadManager : OnSpiderListener {
}
interface DownloadInfoListener {
/**
* Add the special info to the special position
*/
fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
/**
* The special info is changed
*/
fun onUpdate(info: DownloadInfo)
fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>)
/**
* Maybe all data is changed, but size is the same
*/
fun onUpdateAll()
/**
* Maybe all data is changed, maybe list is changed
*/
fun onReload()
/**
* Remove the special info from the special position
*/
fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
}
interface DownloadListener {
@@ -870,7 +940,12 @@ object DownloadManager : OnSpiderListener {
Log.e(TAG, "Current task is null, but it should not be")
} else {
info.total = mPages
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
}
}
@@ -899,7 +974,12 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onGetPage(info)
}
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
}
}
@@ -912,7 +992,12 @@ object DownloadManager : OnSpiderListener {
info.finished = mFinished
info.downloaded = mDownloaded
info.total = mTotal
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
}
}
@@ -951,7 +1036,12 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onFinish(info)
}
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
// Start next download
ensureDownload()
}
@@ -1037,7 +1127,12 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onDownload(info)
}
mDownloadInfoListener?.onUpdate(info)
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
}
mBytesRead = 0
if (!mStop) {
@@ -135,9 +135,9 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
): Boolean {
return if (isCronetSupported) {
cronetRequest(url, referer) {
setCacheDisabled(true)
disableCache()
}.execute { info ->
val headers = info.headers.asMap
val headers = info.allHeaders
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
val length = headers["Content-Length"]!!.first().toLong()
saveResponseMeta(index, type, length) { file ->
@@ -20,8 +20,6 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
@@ -30,12 +28,7 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.HeartBroken
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhDB
@@ -62,7 +55,6 @@ import com.hippo.ehviewer.util.requestPermission
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
@@ -320,6 +312,7 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
1 -> withUIContext {
if (downloaded) {
confirmRemoveDownload(info)
DownloadManager.deleteDownload(info.gid)
} else {
CommonOperations.startDownload(this@with, info, false)
}
@@ -349,28 +342,12 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
private const val MAX_FAVNOTE_CHAR = 200
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
var checked by mutableStateOf(Settings.removeImageFiles)
awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
onDismiss = onDismiss,
text = {
Column {
Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty()))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text(text = stringResource(id = R.string.download_remove_dialog_check_text))
}
}
},
)
Settings.removeImageFiles = checked
withIOContext {
DownloadManager.deleteDownload(info.gid, checked)
}
}
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) = awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
text = { Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty())) },
)
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
@@ -197,7 +197,7 @@ class MainActivity : EhActivity() {
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
val handled = onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
@@ -398,10 +398,6 @@ class MainActivity : EhActivity() {
}
}
fun clearNavCheckedItem() {
binding.navView.setCheckedItem(R.id.nav_stub)
}
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
}
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
@@ -72,7 +71,7 @@ class SearchLayout @JvmOverloads constructor(
}
}
Mdc3Theme {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).imePadding().verticalScroll(rememberScrollState())) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).verticalScroll(rememberScrollState())) {
AnimatedVisibility(visible = isNormalMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
@@ -28,7 +28,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.toIntOrDefault
@Composable
fun SearchAdvanced(
@@ -56,6 +55,9 @@ fun SearchAdvanced(
onExpandedChange = { expanded = !expanded },
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
SideEffect {
softwareKeyboardController?.hide()
}
OutlinedTextField(
modifier = Modifier.menuAnchor(),
readOnly = true,
@@ -69,9 +71,6 @@ fun SearchAdvanced(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
SideEffect {
softwareKeyboardController?.hide()
}
minRatingItems.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
@@ -89,18 +88,18 @@ fun SearchAdvanced(
Checkbox(checked = enabled, onCheckedChange = { enabled = it })
Text(text = stringResource(id = R.string.search_sp), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.fromPage != -1) state.fromPage.toString() else "",
onValueChange = { onStateChanged(state.copy(fromPage = it.toIntOrDefault(-1))) },
onValueChange = { onStateChanged(state.copy(fromPage = it.toInt())) },
modifier = Modifier.width(96.dp).padding(16.dp),
singleLine = true,
enabled = enabled,
)
Text(text = stringResource(id = R.string.search_sp_to), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.toPage != -1) state.toPage.toString() else "",
onValueChange = { onStateChanged(state.copy(toPage = it.toIntOrDefault(-1))) },
onValueChange = { onStateChanged(state.copy(toPage = it.toInt())) },
modifier = Modifier.width(96.dp).padding(16.dp),
singleLine = true,
enabled = enabled,
@@ -21,7 +21,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
@@ -29,14 +28,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
@@ -112,7 +109,6 @@ fun GalleryInfoListItem(
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
textAlign = TextAlign.Center,
)
Row(
@@ -202,11 +198,10 @@ fun GalleryInfoGridItem(
modifier = Modifier.aspectRatio(aspect).fillMaxWidth(),
contentScale = ContentScale.Crop,
)
val categoryColor = EhUtils.getCategoryColor(info.category)
Badge(
modifier = Modifier.align(Alignment.TopEnd).width(32.dp).height(24.dp),
containerColor = categoryColor,
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
containerColor = EhUtils.getCategoryColor(info.category),
contentColor = MaterialTheme.colorScheme.onSurface,
) {
simpleLang?.let {
Text(text = it.uppercase())
@@ -26,10 +26,7 @@ import androidx.core.view.GravityCompat
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.material.sidesheet.SideSheetDialog
import com.hippo.ehviewer.ui.MainActivity
@@ -158,10 +155,8 @@ abstract class BaseScene : Fragment() {
createDrawerView(savedInstanceState)?.let {
sideSheetDialog = SideSheetDialog(requireContext()).apply {
window?.decorView?.apply {
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
setViewTreeLifecycleOwner(viewLifecycleOwner)
setViewTreeSavedStateRegistryOwner(this@BaseScene)
}
setContentView(it)
}
@@ -46,9 +46,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -56,11 +54,11 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
import coil.compose.AsyncImage
@@ -81,11 +79,11 @@ import com.hippo.ehviewer.databinding.ItemDownloadBinding
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
import com.hippo.ehviewer.databinding.SceneDownloadBinding
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.download.downloadDir
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
@@ -100,10 +98,8 @@ import com.hippo.ehviewer.ui.main.requestOf
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.LongList
import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -112,21 +108,20 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.pxToDp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
import com.hippo.ehviewer.download.DownloadManager as downloadManager
@SuppressLint("RtlHardcoded")
class DownloadsScene :
SearchBarScene(),
BaseToolbarScene(),
DownloadInfoListener,
OnClickFabListener {
/*---------------
Whole life cycle
---------------*/
private var mKeyword: String? = null
private var mLabel: String? = null
private val mList get() = mAdapter?.currentList
private var mList: List<DownloadInfo>? = null
/*---------------
View life cycle
@@ -137,23 +132,18 @@ class DownloadsScene :
private var mAdapter: DownloadAdapter? = null
private var mItemTouchHelper: ItemTouchHelper? = null
private val tracker get() = mAdapter!!.tracker!!
private var mGid = -1L
private var mInitPosition = -1
private var mLabelAdapter: DownloadLabelAdapter? = null
private lateinit var mLabels: MutableList<String>
private var mType = -1
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun initLabels() {
context ?: return
val listLabel = downloadManager.labelList
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
// Add "All" and "Default" label names
mLabels = arrayListOf(
getString(R.string.download_all),
getString(R.string.default_download_label_name),
)
mLabels.add(getString(R.string.download_all))
mLabels.add(getString(R.string.default_download_label_name))
listLabel.forEach {
mLabels.add(it.label)
}
@@ -171,7 +161,18 @@ class DownloadsScene :
val info = DownloadManager.getDownloadInfo(gid)
if (null != info) {
mLabel = info.label
mGid = gid
updateForLabel()
updateView()
// Get position
if (null != mList) {
val position = mList!!.indexOf(info)
if (position >= 0 && null != _binding) {
binding.recyclerView.scrollToPosition(position)
} else {
mInitPosition = position
}
}
return true
}
}
@@ -180,6 +181,7 @@ class DownloadsScene :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DownloadManager.addDownloadInfoListener(this)
if (savedInstanceState == null) {
onInit()
} else {
@@ -187,54 +189,50 @@ class DownloadsScene :
}
}
private fun filter(info: DownloadInfo) = info.run {
(mType == -1 || state == mType) &&
mKeyword?.let {
title.containsIgnoreCase(it) || titleJpn.containsIgnoreCase(it) ||
uploader.containsIgnoreCase(it)
} ?: true
}
private fun updateInfoList() {
mAdapter?.run {
val list = when (mLabel) {
null -> DownloadManager.allInfoList
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
?: DownloadManager.allInfoList.also { mLabel = null }
}.mapNotNull { it.takeIf(::filter)?.copy(downloadInfo = it.downloadInfo.copy()) }
submitList(list) {
if (mGid != -1L) {
val position = list.indexOfFirst { it.gid == mGid }
if (position != -1) {
binding.recyclerView.scrollToPosition(position)
}
mGid = -1L
}
updateView()
}
}
override fun onDestroy() {
super.onDestroy()
mList = null
DownloadManager.removeDownloadInfoListener(this)
}
@SuppressLint("NotifyDataSetChanged")
private fun updateForLabel() {
updateInfoList()
val list = when (mLabel) {
null -> DownloadManager.allInfoList
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
?: DownloadManager.allInfoList.also { mLabel = null }
}
mList = if (mType != -1) {
list.filter { it.state == mType }
} else {
list
}
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
Settings.recentDownloadLabel = mLabel
}
private fun updateTitle() {
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
setSearchBarHint(title)
setEditTextHint(getString(R.string.search_bar_hint, title))
setTitle(
getString(
R.string.scene_download_title,
if (mLabel != null) mLabel else getString(R.string.download_all),
),
)
}
private fun onInit() {
if (!handleArguments(arguments)) {
mLabel = Settings.recentDownloadLabel
updateForLabel()
}
}
private fun onRestore(savedInstanceState: Bundle) {
mLabel = savedInstanceState.getString(KEY_LABEL)
updateForLabel()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -249,7 +247,6 @@ class DownloadsScene :
savedInstanceState: Bundle?,
): View {
_binding = SceneDownloadBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
binding.run {
setLiftOnScrollTargetView(recyclerView)
mViewTransition = ViewTransition(content, tip)
@@ -291,6 +288,11 @@ class DownloadsScene :
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
// Cancel change animation
val itemAnimator = recyclerView.itemAnimator
if (itemAnimator is SimpleItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val decoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
@@ -298,14 +300,20 @@ class DownloadsScene :
}
}
recyclerView.addItemDecoration(decoration)
if (mInitPosition >= 0) {
recyclerView.scrollToPosition(mInitPosition)
mInitPosition = -1
}
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ACTION_STATE_DRAG) {
tracker.clearSelection()
}
}
override fun isLongPressDragEnabled() = false
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
}
override fun onMove(
recyclerView: RecyclerView,
@@ -317,24 +325,18 @@ class DownloadsScene :
if (fromPosition == toPosition) {
return false
}
val fromItem = mList!![fromPosition]
val toItem = mList!![toPosition]
val list = DownloadManager.moveDownload(fromItem, toItem)
updateInfoList()
val list = when (mLabel) {
null -> DownloadManager.moveDownload(fromPosition, toPosition)
getString(R.string.default_download_label_name) -> DownloadManager.moveDownload(null, fromPosition, toPosition)
else -> DownloadManager.moveDownload(mLabel, fromPosition, toPosition)
}
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
lifecycleScope.launchIO {
EhDB.updateDownloadInfo(list)
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mList!![position]
lifecycleScope.launchUI {
dialogState.confirmRemoveDownload(info) {
mAdapter!!.notifyItemChanged(position)
}
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
mItemTouchHelper!!.attachToRecyclerView(recyclerView)
fastScroller.attachToRecyclerView(recyclerView)
@@ -347,23 +349,15 @@ class DownloadsScene :
fabLayout.setAutoCancel(false)
fabLayout.setOnClickFabListener(this@DownloadsScene)
addAboveSnackView(fabLayout)
updateForLabel()
}
viewLifecycleOwner.lifecycleScope.launch {
DownloadManager.stateFlow().collectLatest {
updateInfoList()
}
}
setOnApplySearch {
mKeyword = it.takeUnless { it.isEmpty() }
updateInfoList()
updateView()
}
return binding.root
}
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateTitle()
setNavigationIcon(R.drawable.ic_baseline_menu_24)
}
override fun onDestroyView() {
@@ -399,7 +393,8 @@ class DownloadsScene :
mType + 1,
) { dialog: DialogInterface, which: Int ->
mType = which - 1
updateInfoList()
updateForLabel()
updateView()
dialog.dismiss()
}
.show()
@@ -452,7 +447,7 @@ class DownloadsScene :
}
}
private fun updateView() {
fun updateView() {
if (mViewTransition != null) {
if (mList.isNullOrEmpty()) {
mViewTransition!!.showView(1)
@@ -600,6 +595,61 @@ class DownloadsScene :
}
}
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
if (mList !== list) {
return
}
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyItemInserted(position)
}
updateView()
}
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
if (mLabel != null && mList !== list) {
return
}
val index = mList!!.indexOf(info)
lifecycleScope.launchUI {
if (index >= 0 && mAdapter != null) {
mAdapter!!.notifyItemChanged(index, PAYLOAD_STATE)
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onUpdateAll() {
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onReload() {
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
updateView()
}
}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
if (mList !== list) {
return
}
lifecycleScope.launchUI {
if (mAdapter != null) {
mAdapter!!.notifyItemRemoved(position)
}
updateView()
}
}
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
RecyclerView.ViewHolder(binding.root)
@@ -610,14 +660,17 @@ class DownloadsScene :
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label = mLabels[position].takeUnless { position == 0 }
if (mLabel != label) {
mKeyword = null
setSearchBarText(null)
mLabel = label
val label1: String? = if (position == 0) {
null
} else {
mLabels[position]
}
if (mLabel != label1) {
mLabel = label1
updateForLabel()
updateView()
closeSideSheet()
}
closeSideSheet()
}
holder.binding.edit.setOnClickListener {
val context = context
@@ -655,7 +708,7 @@ class DownloadsScene :
}
holder.binding.run {
if (list != null) {
text.text = "$label [${list.size}]"
text.text = label + " [" + list.size + "]"
} else {
text.text = label
}
@@ -810,7 +863,6 @@ class DownloadsScene :
Text(
text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelLarge,
)
@@ -900,23 +952,17 @@ class DownloadsScene :
}
}
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
return if (oldItem.downloadInfo != newItem.downloadInfo) {
PAYLOAD_DOWNLOAD_INFO
} else {
super.getChangePayload(oldItem, newItem)
}
}
}
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
private inner class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
private val mInflater: LayoutInflater = layoutInflater
var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int) = getItem(position).gid
override fun getItemId(position: Int): Long {
return if (mList == null || position < 0 || position >= mList!!.size) {
0
} else {
mList!![position].gid
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
@@ -925,8 +971,10 @@ class DownloadsScene :
}
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val info = getItem(position)
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
mList?.let {
val info = it[position]
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
}
override fun onBindViewHolder(
@@ -936,15 +984,18 @@ class DownloadsScene :
) {
payloads.forEach { payload ->
when (payload) {
PAYLOAD_DOWNLOAD_INFO -> {
val info = getItem(position)
holder.bindForState(info)
PAYLOAD_STATE -> {
mList?.let { holder.bindForState(it[position]) }
return
}
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return if (mList == null) 0 else mList!!.size
}
}
private inner class RenameLabelDialogHelper(
@@ -979,6 +1030,7 @@ class DownloadsScene :
if (mLabel == mOriginalLabel) {
mLabel = text
updateForLabel()
updateView()
}
}
}
@@ -1067,6 +1119,7 @@ class DownloadsScene :
withUIContext {
mLabel = null
updateForLabel()
updateView()
mLabelAdapter!!.notifyItemRemoved(position)
}
}
@@ -1084,7 +1137,7 @@ class DownloadsScene :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
private const val PAYLOAD_DOWNLOAD_INFO = 0
private const val PAYLOAD_STATE = 0
}
}
@@ -51,8 +51,6 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
import com.google.android.material.datepicker.CompositeDateValidator
@@ -80,18 +78,17 @@ import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleHandler
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import java.time.Instant
@@ -163,10 +160,6 @@ class FavoritesScene : SearchBarScene() {
}
}
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun onItemClick(position: Int) {
// Skip if in search mode
if (!tracker.isInCustomChoice) {
@@ -198,11 +191,10 @@ class FavoritesScene : SearchBarScene() {
}
if (keyword.isNullOrEmpty()) {
setSearchBarHint(getString(R.string.favorites_title, favCatName))
setSearchBarText(null)
} else {
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
}
setEditTextHint(getString(R.string.search_bar_hint, favCatName))
setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName))
Settings.recentFavCat = urlBuilder.favCat
}
@@ -230,7 +222,6 @@ class FavoritesScene : SearchBarScene() {
savedInstanceState: Bundle?,
): View {
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
setOnApplySearch {
if (!tracker.isInCustomChoice) {
switchFav(urlBuilder.favCat, it)
@@ -336,8 +327,8 @@ class FavoritesScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
viewLifecycleOwner.lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
adapter.addLoadStateListener {
lifecycleScope.launchUI {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -390,40 +381,7 @@ class FavoritesScene : SearchBarScene() {
}
switchFav(Settings.recentFavCat)
}
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(0, ItemTouchHelper.LEFT)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mAdapter!!.peek(position)!!
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.delete,
dismissText = android.R.string.cancel,
title = R.string.delete_favorites_dialog_title,
onDismiss = { mAdapter!!.notifyItemChanged(position) },
) {
Text(text = stringResource(id = R.string.delete_favorites_dialog_message, 1))
}
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
EhDB.removeLocalFavorites(info)
} else {
EhEngine.modifyFavorites(info.gid, info.token)
}
}
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
allowEmptySearch = false
return binding.root
}
@@ -481,16 +439,6 @@ class FavoritesScene : SearchBarScene() {
}
}
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
showActionFab(true)
}
private fun showGoToDialog() {
context ?: return
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
@@ -580,3 +528,5 @@ class FavoritesScene : SearchBarScene() {
}
}
}
private const val ANIMATE_TIME = 300L
@@ -68,6 +68,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@@ -105,7 +106,6 @@ import arrow.core.partially1
import coil.imageLoader
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -123,7 +123,6 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.client.parser.TorrentResult
@@ -160,7 +159,6 @@ import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
import com.hippo.ehviewer.util.AppHelper
import com.hippo.ehviewer.util.ExceptionUtils
@@ -343,24 +341,6 @@ class GalleryDetailScene : BaseScene() {
}
}
private fun actionClearCache() {
val gd = composeBindingGD ?: return
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.clear_all,
dismissText = android.R.string.cancel,
title = R.string.clear_image_cache,
) {
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
}
(0..<gd.pages).forEach {
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
showTip(R.string.image_cache_cleared, LENGTH_SHORT)
}
}
private fun actionOpenInOtherApp() {
val url = galleryDetailUrl ?: return
val activity: Activity = mainActivity ?: return
@@ -486,13 +466,6 @@ class GalleryDetailScene : BaseScene() {
actionRefresh()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.clear_image_cache)) },
onClick = {
dropdown = false
actionClearCache()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
onClick = {
@@ -739,7 +712,7 @@ class GalleryDetailScene : BaseScene() {
stringResource(id = R.string.not_favorited)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
FilledTertiaryIconToggleButton(
FilledTonalIconToggleButton(
checked = favored,
onCheckedChange = { modifyFavourite() },
) {
@@ -15,6 +15,7 @@
*/
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
@@ -65,12 +66,10 @@ import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_IMAGE_SEARCH
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_UPLOADER
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
import com.hippo.ehviewer.client.exception.CloudflareBypassException
import com.hippo.ehviewer.client.exception.EhException
@@ -99,11 +98,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.getParcelableCompat
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
@@ -129,10 +130,10 @@ class VMStorage1 : ViewModel() {
if (urlBuilder.mode == MODE_TOPLIST) {
// TODO: Since we know total pages, let pager support jump
val key = (params.key ?: urlBuilder.mJumpTo ?: "0").toInt()
val prev = (key - 1).takeIf { it > 0 }
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
val prev = if (key != 0) key - 1 else null
val next = if (key != 199) key + 1 else null
runSuspendCatching {
urlBuilder.setJumpTo(key)
urlBuilder.setJumpTo(key.toString())
EhEngine.getGalleryList(urlBuilder.build())
}.onFailure {
return@withIOContext LoadResult.Error(it)
@@ -155,7 +156,7 @@ class VMStorage1 : ViewModel() {
}
}
val r = runSuspendCatching {
if (MODE_IMAGE_SEARCH == urlBuilder.mode) {
if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.mode) {
EhEngine.imageSearch(
File(urlBuilder.imagePath!!),
urlBuilder.isUseSimilarityScan,
@@ -191,15 +192,23 @@ class GalleryListScene : SearchBarScene() {
}
private var _binding: SceneGalleryListBinding? = null
private val binding get() = _binding!!
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
private val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
binding.fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private var fabAnimator: ViewPropertyAnimator? = null
private var mViewTransition: ViewTransition? = null
private var mAdapter: GalleryAdapter? = null
lateinit var mQuickSearchList: MutableList<QuickSearch>
private var mHideActionFabSlop = 0
private var mShowActionFab = true
private var mState = State.NORMAL
override val fabLayout get() = binding.fabLayout
override fun getMenuResId(): Int {
return R.menu.scene_gallery_list_searchbar_menu
}
@@ -214,6 +223,7 @@ class GalleryListScene : SearchBarScene() {
ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder()
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
}
args?.getString(KEY_GOTO)?.let { mUrlBuilder.mNext = it }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -256,7 +266,10 @@ class GalleryListScene : SearchBarScene() {
val category = mUrlBuilder.category
val mode = mUrlBuilder.mode
val isPopular = mode == MODE_WHATS_HOT
mIsTopList = mode == MODE_TOPLIST
val isTopList = mode == MODE_TOPLIST
if (isTopList != mIsTopList) {
mIsTopList = isTopList
}
// Update fab visibility
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
@@ -282,14 +295,6 @@ class GalleryListScene : SearchBarScene() {
title = resources.getString(R.string.search)
}
setSearchBarHint(title)
when (mode) {
MODE_NORMAL -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
mainActivity?.clearNavCheckedItem()
}
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
else -> Unit
}
}
private val dialogState = DialogState()
@@ -363,8 +368,9 @@ class GalleryListScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
launch {
adapter.loadStateFlow.collectLatest {
val noWatch = getString(R.string.gallery_list_empty_hit_subscription)
adapter.addLoadStateListener {
lifecycleScope.launchUI {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -391,7 +397,11 @@ class GalleryListScene : SearchBarScene() {
delay(500)
binding.refreshLayout.isRefreshing = false
if (mAdapter?.itemCount == 0) {
binding.tip.text = empty
if (mUrlBuilder.mode == MODE_SUBSCRIPTION) {
binding.tip.text = noWatch
} else {
binding.tip.text = empty
}
transition.showView(2)
} else {
transition.showView(0)
@@ -442,14 +452,12 @@ class GalleryListScene : SearchBarScene() {
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
when (position) {
0 -> showGoToDialog()
1 -> { // First
1 -> {
mUrlBuilder.setIndex(null, true)
mUrlBuilder.mJumpTo = null
mAdapter?.refresh()
}
2 -> { // Last
2 -> {
if (mIsTopList) {
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
mAdapter?.refresh()
} else {
mUrlBuilder.setIndex("1", false)
@@ -473,6 +481,7 @@ class GalleryListScene : SearchBarScene() {
}
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
addAboveSnackView(binding.fabLayout)
mSearchFab.setOnClickListener { onApplySearch() }
// Update list url builder
onUpdateUrlBuilder()
@@ -511,7 +520,7 @@ class GalleryListScene : SearchBarScene() {
val context = context ?: return
// Can't add image search as quick search
if (MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
showTip(R.string.image_search_not_quick_search, LENGTH_LONG)
return
}
@@ -635,9 +644,9 @@ class GalleryListScene : SearchBarScene() {
private fun showGoToDialog() {
mAdapter ?: return
if (mIsTopList) {
val page = mUrlBuilder.mJumpTo?.toIntOrNull() ?: 0
val pages = 200
val title = getString(R.string.go_to)
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
val hint = getString(R.string.go_to_hint, 1, 200)
lifecycleScope.launch {
val text = dialogState.awaitInputText(
title = title,
@@ -650,13 +659,13 @@ class GalleryListScene : SearchBarScene() {
}.onFailure {
return@awaitInputText getString(R.string.error_invalid_number)
}.getOrThrow()
if (goTo !in 0..<TOPLIST_PAGES) {
if (goTo < 0 || goTo >= pages) {
getString(R.string.error_out_of_range)
} else {
null
}
}.trim().toInt() - 1
mUrlBuilder.setJumpTo(text)
mUrlBuilder.setJumpTo(text.toString())
mAdapter?.refresh()
}
} else {
@@ -687,21 +696,13 @@ class GalleryListScene : SearchBarScene() {
}
override fun onSearchViewExpanded() {
if (mState == State.NORMAL) {
hideActionFab(true)
} else {
hideSearchFab(true)
}
super.onSearchViewExpanded()
if (mState == State.NORMAL) selectSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
if (mState == State.NORMAL) {
showActionFab(true)
} else {
showSearchFab(true)
}
if (mState == State.NORMAL) selectActionFab(true)
}
private fun showActionFab() {
@@ -737,6 +738,70 @@ class GalleryListScene : SearchBarScene() {
}
}
private fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
if (animation) {
val fab: View? = binding.fabLayout.primaryFab
val delay: Long
if (View.INVISIBLE == fab!!.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
binding.fabLayout.setExpanded(expanded = false, animation = true)
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
}
mSearchFab.visibility = View.VISIBLE
mSearchFab.rotation = -45.0f
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
private fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
if (animation) {
val delay: Long
if (View.INVISIBLE == mSearchFab.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
}
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.rotation = -45.0f
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
}
private fun setState(state: State) {
setState(state, true)
}
@@ -798,35 +863,24 @@ class GalleryListScene : SearchBarScene() {
private fun onApplySearch(query: String?) {
_binding ?: return
lifecycleScope.launchIO {
val builder = ListUrlBuilder()
val oldMode = mUrlBuilder.mode
if (mState == State.SEARCH || mState == State.SEARCH_SHOW_LIST) {
try {
binding.searchLayout.formatListUrlBuilder(builder, query)
binding.searchLayout.formatListUrlBuilder(mUrlBuilder, query)
} catch (e: EhException) {
showTip(e.message, LENGTH_LONG)
return@launchIO
}
} else {
val oldMode = mUrlBuilder.mode
// If it's MODE_SUBSCRIPTION, keep it
val newMode = if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
builder.mode = newMode
builder.keyword = query
mUrlBuilder.reset()
mUrlBuilder.mode = newMode
mUrlBuilder.keyword = query
}
withUIContext {
when (oldMode) {
MODE_TOPLIST, MODE_WHATS_HOT -> {
// Wait for search view to hide
delay(300)
navAnimated(R.id.galleryListScene, builder.toStartArgs())
}
else -> {
mUrlBuilder = builder
onUpdateUrlBuilder()
mAdapter?.refresh()
}
}
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
}
}
@@ -845,23 +899,26 @@ class GalleryListScene : SearchBarScene() {
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<QsDrawerHolder>() {
private val keywords = intArrayOf(11, 12, 13, 15)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
if (!mIsTopList) {
if (!mIsTopList) {
holder.itemView.setOnClickListener {
val q = mQuickSearchList[holder.bindingAdapterPosition]
// Navigate to galleryListScene to clear nav checked item
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
} else {
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
mUrlBuilder.set(q)
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
} else {
val keywords = intArrayOf(11, 12, 13, 15)
holder.itemView.setOnClickListener {
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
setState(State.NORMAL)
closeSideSheet()
}
return holder
}
@@ -991,6 +1048,7 @@ class GalleryListScene : SearchBarScene() {
companion object {
const val KEY_ACTION = "action"
const val KEY_GOTO = "goto"
const val ACTION_HOMEPAGE = "action_homepage"
const val ACTION_SUBSCRIPTION = "action_subscription"
const val ACTION_WHATS_HOT = "action_whats_hot"
@@ -998,6 +1056,7 @@ class GalleryListScene : SearchBarScene() {
const val ACTION_LIST_URL_BUILDER = "action_list_url_builder"
const val KEY_LIST_URL_BUILDER = "list_url_builder"
const val KEY_STATE = "state"
private const val ANIMATE_TIME = 300L
fun ListUrlBuilder.toStartArgs() = bundleOf(
KEY_ACTION to ACTION_LIST_URL_BUILDER,
KEY_LIST_URL_BUILDER to this,
@@ -1005,8 +1064,6 @@ class GalleryListScene : SearchBarScene() {
}
}
private const val TOPLIST_PAGES = 200
@Parcelize
enum class State : Parcelable {
NORMAL, SIMPLE_SEARCH, SEARCH, SEARCH_SHOW_LIST
@@ -1,6 +1,5 @@
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@@ -41,10 +40,7 @@ import com.hippo.ehviewer.dao.Search
import com.hippo.ehviewer.dao.SearchDao
import com.hippo.ehviewer.databinding.SceneSearchbarBinding
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.FabLayout
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -61,18 +57,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
private val mSearchDatabase = searchDatabase.searchDao()
private var onApplySearch: (String) -> Unit = {}
protected val mSearchFab get() = binding.searchFab
protected abstract val fabLayout: FabLayout
protected var mShowActionFab = true
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
@@ -115,7 +99,6 @@ 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
@@ -164,87 +147,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
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
}
}
fun showActionFab(animation: Boolean, delay: Long = ANIMATE_TIME) {
mShowActionFab = true
if (animation) {
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
}
}
fun hideActionFab(animation: Boolean): Long {
mShowActionFab = false
return if (animation) {
val fab = fabLayout.primaryFab!!
if (View.INVISIBLE == fab.visibility) {
0L
} else {
fabLayout.setExpanded(expanded = false, animation = true)
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
ANIMATE_TIME
}
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
0L
}
}
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
}
@@ -427,5 +329,3 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
}
}
}
const val ANIMATE_TIME = 300L
@@ -112,7 +112,7 @@ fun AboutScreen() {
title = stringResource(id = R.string.use_ci_update_channel),
value = Settings::useCIUpdateChannel,
)
SimpleMenuPreferenceInt(
SimpleMenuPreferenceInt2(
title = stringResource(id = R.string.auto_updates),
entry = R.array.update_frequency,
entryValueRes = R.array.update_frequency_values,
@@ -126,6 +126,11 @@ fun AdvancedScreen() {
entryValueRes = R.array.app_language_entry_values,
value = Settings::language,
)
SwitchPreference(
title = stringResource(id = R.string.settings_advanced_enable_quic),
summary = stringResource(id = R.string.settings_advanced_enable_quic_summary),
value = Settings::enableQuic,
)
SwitchPreference(
title = stringResource(id = R.string.preload_thumb_aggressively),
value = Settings::preloadThumbAggressively,
@@ -193,27 +193,6 @@ fun EhScreen() {
summary = stringResource(id = R.string.settings_my_tags_summary),
) { navController.navigate(MYTAGS_SCREEN) }
}
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SimpleMenuPreferenceInt(
title = stringResource(id = rikka.core.R.string.dark_theme),
entry = R.array.night_mode_entries,
@@ -224,10 +203,6 @@ fun EhScreen() {
title = stringResource(id = R.string.black_dark_theme),
value = Settings::blackDarkTheme,
)
SwitchPreference(
title = stringResource(id = R.string.harmonize_category_color),
value = Settings::harmonizeCategoryColor,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_launch_page),
entry = R.array.launch_page_entries,
@@ -269,6 +244,27 @@ fun EhScreen() {
entryValueRes = R.array.thumb_resolution_entry_values,
value = thumbResolution,
)
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SwitchPreference(
title = stringResource(id = R.string.settings_eh_force_eh_thumb),
summary = stringResource(id = R.string.settings_eh_force_eh_thumb_summary),
@@ -114,6 +114,20 @@ fun HtmlPreference(title: String, summary: AnnotatedString? = null, onClick: ()
@Composable
fun SimpleMenuPreferenceInt(title: String, summary: String? = null, @ArrayRes entry: Int, @ArrayRes entryValueRes: Int, value: MutableState<Int>) {
val entryArray = stringArrayResource(id = entry)
val valuesArray = stringArrayResource(id = entryValueRes)
val map = remember {
val iter = entryArray.iterator()
valuesArray.associateWith { iter.next() }
}
var v by value
fun set(new: String) { v = new.toInt() }
check(entryArray.size == valuesArray.size)
DropDownPref(title = title, summary = summary, defaultValue = v.toString(), onValueChange = ::set, useSelectedAsSummary = summary.isNullOrBlank(), entries = map)
}
@Composable
fun SimpleMenuPreferenceInt2(title: String, summary: String? = null, @ArrayRes entry: Int, @ArrayRes entryValueRes: Int, value: MutableState<Int>) {
val entryArray = stringArrayResource(id = entry)
val valuesArray = integerArrayResource(id = entryValueRes)
val map = remember {
@@ -3,6 +3,7 @@ package com.hippo.ehviewer.ui.tools
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
@@ -14,6 +15,7 @@ private const val QuarterCircle = PI / 2
@Composable
fun CircularLayout(
modifier: Modifier = Modifier,
radius: Dp,
placeFirstItemInCenter: Boolean = false,
content: @Composable () -> Unit,
) {
@@ -21,7 +23,7 @@ fun CircularLayout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val radiusPx = constraints.maxWidth * 0.4
val radiusPx = radius.toPx()
val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables
@@ -3,10 +3,12 @@ package com.hippo.ehviewer.ui.tools
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -41,11 +43,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.jamal.composeprefs3.ui.ifNotNullThen
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -146,25 +148,18 @@ class DialogState {
@StringRes confirmText: Int? = null,
@StringRes dismissText: Int? = null,
@StringRes title: Int? = null,
onDismiss: () -> Unit = {},
text: (@Composable () -> Unit)? = null,
) {
return dialog { cont ->
AlertDialog(
onDismissRequest = {
onDismiss()
cont.cancel()
},
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }) {
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
}
},
dismissButton = dismissText.ifNotNullThen {
TextButton(onClick = {
onDismiss()
cont.cancel()
}) {
TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(id = dismissText!!))
}
},
@@ -183,7 +178,6 @@ class DialogState {
}
AlertDialog(
onDismissRequest = { cont.cancel() },
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
@@ -201,22 +195,12 @@ class DialogState {
vararg items: String,
@StringRes title: Int,
): Int = showNoButton {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
style = MaterialTheme.typography.headlineSmall,
)
LazyColumn {
itemsIndexed(items) { index, text ->
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Text(
text = text,
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
style = MaterialTheme.typography.titleMedium,
)
}
Column(modifier = Modifier.padding(24.dp)) {
Text(text = stringResource(id = title), style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.size(16.dp))
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
items.forEachIndexed { index, text ->
Text(text = text, modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth().padding(vertical = 8.dp), style = MaterialTheme.typography.titleMedium)
}
}
}
@@ -249,10 +233,12 @@ class DialogState {
@StringRes title: Int,
@StringRes hint: Int,
maxChar: Int,
adjustTextPosition: Boolean = true,
): Pair<Int, String> = showNoButton(false) {
Column {
Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
CircularLayout(
radius = 112.dp,
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
placeFirstItemInCenter = true,
) {
@@ -260,7 +246,8 @@ class DialogState {
TextField(
value = note,
onValueChange = { note = it },
modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F),
modifier = Modifier.width(128.dp),
textStyle = TextStyle(fontFamily = FontFamily.Monospace),
label = { Text(text = stringResource(id = hint)) },
trailingIcon = {
if (note.isNotEmpty()) {
@@ -276,6 +263,7 @@ class DialogState {
textAlign = TextAlign.End,
)
},
maxLines = 6,
shape = ShapeDefaults.ExtraSmall,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
@@ -284,18 +272,16 @@ class DialogState {
)
items.forEachIndexed { index, (icon, text) ->
Column(
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) }
.fillMaxWidth(0.2F),
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) },
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(
text = text,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.bodySmall,
)
if (index == 0 && adjustTextPosition) {
Text(text = text, style = MaterialTheme.typography.bodySmall)
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
} else {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(text = text, style = MaterialTheme.typography.bodySmall)
}
}
}
}
@@ -60,12 +60,12 @@ object AppConfig {
return FileUtils.createTempFile(tempDir, null)
}
fun saveParseErrorBody(e: ParseException, body: String) {
fun saveParseErrorBody(e: ParseException) {
val dir = externalParseErrorDir ?: return
File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt").writer().use {
it.write(e.message)
it.appendLine()
it.append(body)
it.append(e.body)
}
}
@@ -1,8 +1,7 @@
package com.hippo.ehviewer.util
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.Settings
@get:ChecksSdkIntAtLeast(Build.VERSION_CODES.S, extension = 7)
val isCronetSupported: Boolean
get() = isAtLeastSExtension7
get() = Settings.enableQuic && isAtLeastQ && CronetProviderInstaller.isInstalled()
@@ -1,8 +1,5 @@
package com.hippo.ehviewer.util
import android.os.Build
import android.os.ext.SdkExtensions
val isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val isAtLeastR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
val isAtLeastSExtension7 = isAtLeastR && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7
@@ -21,5 +21,3 @@ fun String.toLongOrDefault(defaultValue: Long): Long {
fun String.toFloatOrDefault(defaultValue: Float): Float {
return toFloatOrNull() ?: defaultValue
}
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
@@ -57,9 +57,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
override fun onTouchEvent(e: MotionEvent): Boolean {
if (scrollState == SCROLL_STATE_IDLE) {
detector.onTouchEvent(e)
}
detector.onTouchEvent(e)
return super.onTouchEvent(e)
}
+10 -7
View File
@@ -17,26 +17,29 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_filter"
android:icon="@drawable/v_filter_dark_x24"
android:title="@string/download_filter"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_open_side_sheet"
android:icon="@drawable/ic_outline_label_24"
android:title="@string/download_labels"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_filter"
android:title="@string/download_filter"
app:showAsAction="never" />
<item
android:id="@+id/action_start_all"
android:icon="@drawable/v_play_x24"
android:title="@string/download_start_all"
app:showAsAction="never" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_stop_all"
android:icon="@drawable/v_pause_x24"
android:title="@string/download_stop_all"
app:showAsAction="never" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_reset_reading_progress"
+1 -1
View File
@@ -294,7 +294,7 @@
<string name="settings_advanced_dump_logcat_to">Protokolldatei wurde auf %s gespeichert.</string>
<string name="cloud_favorites">Cloud-Favoriten</string>
<string name="local_favorites">Lokale Favoriten</string>
<string name="search_bar_hint">%s durchsuchen</string>
<string name="favorites_search_bar_hint">%s durchsuchen</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
+1 -1
View File
@@ -354,7 +354,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoritos en la nube</string>
<string name="local_favorites">Favoritos locales</string>
<string name="search_bar_hint">Buscar %s</string>
<string name="favorites_search_bar_hint">Buscar %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
+1 -1
View File
@@ -393,7 +393,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoris de cloud</string>
<string name="local_favorites">Favoris locales</string>
<string name="search_bar_hint">Chercher dans %s</string>
<string name="favorites_search_bar_hint">Chercher dans %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
+1 -1
View File
@@ -390,7 +390,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloudのお気に入り</string>
<string name="local_favorites">ローカルのお気に入り</string>
<string name="search_bar_hint">%sを検索</string>
<string name="favorites_search_bar_hint">%sを検索</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
+1 -1
View File
@@ -364,7 +364,7 @@
<!-- Favorites -->
<string name="cloud_favorites">클라우드 즐겨찾기</string>
<string name="local_favorites">로컬 즐겨찾기</string>
<string name="search_bar_hint">%s 검색</string>
<string name="favorites_search_bar_hint">%s 검색</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
+1 -1
View File
@@ -379,7 +379,7 @@
<!-- Favorites -->
<string name="cloud_favorites">รายการโปรดบนเว็บ</string>
<string name="local_favorites">รายการโปรดบนเครื่อง</string>
<string name="search_bar_hint">ค้นหา %s</string>
<string name="favorites_search_bar_hint">ค้นหา %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
+4 -6
View File
@@ -149,9 +149,6 @@
<string name="refresh">刷新</string>
<string name="refresh_original">刷新(原图)</string>
<string name="open_in_other_app">在其他应用中打开</string>
<string name="clear_image_cache">清除图片缓存</string>
<string name="clear_image_cache_confirm">清除该画廊的所有图片缓存?</string>
<string name="image_cache_cleared">已清除图片缓存</string>
<string name="rate_successfully">评分成功</string>
<string name="rate_failed">评分失败</string>
<string name="no_torrents">没有种子</string>
@@ -379,6 +376,8 @@
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
<string name="settings_advanced_app_language_title">App 界面语言</string>
<string name="settings_advanced_enable_quic">[实验性] QUIC 支持</string>
<string name="settings_advanced_enable_quic_summary">启用 QUIC 协议支持(需要重启应用)</string>
<string name="settings_advanced_export_data">导出数据</string>
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
@@ -410,7 +409,7 @@
<!-- Favorites -->
<string name="cloud_favorites">云端收藏</string>
<string name="local_favorites">本地收藏</string>
<string name="search_bar_hint">搜索 %s</string>
<string name="favorites_search_bar_hint">搜索 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">删除收藏</string>
@@ -419,7 +418,7 @@
<string name="default_favorites_collection">默认收藏夹</string>
<string name="default_favorites_warning">启用此项将无法添加收藏备注</string>
<string name="let_me_select">让我选择</string>
<string name="favorite_note">收藏备注</string>
<string name="favorite_note">备注</string>
<string name="collections">收藏夹</string>
<string name="need_sign_in">需要登录</string>
<!-- Progress Scene -->
@@ -476,7 +475,6 @@
<string name="download_service">下载服务</string>
<string name="favorite_name">收藏画廊</string>
<string name="black_dark_theme">纯黑深色主题</string>
<string name="harmonize_category_color">类别颜色适配动态配色</string>
<string name="use_ci_update_channel">使用持续集成(CI)构建频道更新</string>
<string name="download_filter">过滤</string>
<string name="download_all">全部</string>
+1 -1
View File
@@ -419,7 +419,7 @@
<!-- Favorites -->
<string name="cloud_favorites">雲端收藏</string>
<string name="local_favorites">本地收藏</string>
<string name="search_bar_hint">搜索 %s</string>
<string name="favorites_search_bar_hint">搜索 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">刪除收藏</string>
+1 -1
View File
@@ -400,7 +400,7 @@
<!-- Favorites -->
<string name="cloud_favorites">雲端收藏</string>
<string name="local_favorites">本機收藏</string>
<string name="search_bar_hint">搜尋 %s</string>
<string name="favorites_search_bar_hint">搜尋 %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">移除收藏</string>
+20 -20
View File
@@ -21,20 +21,20 @@
<item>@string/settings_eh_list_mode_thumb</item>
</string-array>
<integer-array name="list_mode_entry_values" translatable="false">
<string-array name="list_mode_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-array>
<string-array name="gallery_site_entries" translatable="false">
<item>@string/site_e</item>
<item>@string/site_ex</item>
</string-array>
<integer-array name="gallery_site_entry_values" translatable="false">
<string-array name="gallery_site_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-array>
<string-array name="launch_page_entries" translatable="false">
<item>@string/homepage</item>
@@ -43,22 +43,22 @@
<item>@string/toplist</item>
</string-array>
<integer-array name="launch_page_entry_values" translatable="false">
<string-array name="launch_page_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</integer-array>
</string-array>
<string-array name="detail_size_entries" translatable="false">
<item>@string/settings_eh_detail_size_long</item>
<item>@string/settings_eh_detail_size_short</item>
</string-array>
<integer-array name="detail_size_entry_values" translatable="false">
<string-array name="detail_size_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-array>
<string-array name="thumb_resolution_entries" translatable="false">
<item>@string/settings_eh_thumb_resolution_auto</item>
@@ -66,11 +66,11 @@
<item>300x</item>
</string-array>
<integer-array name="thumb_resolution_entry_values" translatable="false">
<string-array name="thumb_resolution_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
</string-array>
<string-array name="search_min_rating" translatable="false">
<item>@string/unlimited</item>
@@ -87,12 +87,12 @@
<item>7</item>
</string-array>
<integer-array name="multi_thread_download_entry_values" translatable="false">
<string-array name="multi_thread_download_entry_values" translatable="false">
<item>1</item>
<item>3</item>
<item>5</item>
<item>7</item>
</integer-array>
</string-array>
<string-array name="download_delay_entries" translatable="false">
<item>0</item>
@@ -107,7 +107,7 @@
<item>20000</item>
</string-array>
<integer-array name="download_delay_entry_values" translatable="false">
<string-array name="download_delay_entry_values" translatable="false">
<item>0</item>
<item>500</item>
<item>1000</item>
@@ -118,7 +118,7 @@
<item>5000</item>
<item>10000</item>
<item>20000</item>
</integer-array>
</string-array>
<string-array name="preload_image_entries" translatable="false">
<item>3</item>
@@ -129,14 +129,14 @@
<item>17</item>
</string-array>
<integer-array name="preload_image_entry_values" translatable="false">
<string-array name="preload_image_entry_values" translatable="false">
<item>3</item>
<item>5</item>
<item>7</item>
<item>11</item>
<item>13</item>
<item>17</item>
</integer-array>
</string-array>
<string-array name="read_cache_size_entries" translatable="false">
<item>320 MB</item>
@@ -146,13 +146,13 @@
<item>5120 MB</item>
</string-array>
<integer-array name="read_cache_size_entry_values" translatable="false">
<string-array name="read_cache_size_entry_values" translatable="false">
<item>320</item>
<item>640</item>
<item>1280</item>
<item>2560</item>
<item>5120</item>
</integer-array>
</string-array>
<string-array name="filter_entries">
<item>@string/filter_title</item>
@@ -208,11 +208,11 @@
<item>@string/dark_theme_on</item>
</string-array>
<integer-array name="night_mode_values">
<string-array name="night_mode_values">
<item>-1</item>
<item>1</item>
<item>2</item>
</integer-array>
</string-array>
<string-array name="download_state">
<item>@string/download_all</item>
+4 -6
View File
@@ -191,9 +191,6 @@
<string name="refresh">Refresh</string>
<string name="refresh_original">Refresh (Original image)</string>
<string name="open_in_other_app">Open with other app</string>
<string name="clear_image_cache">Clear image cache</string>
<string name="clear_image_cache_confirm">Clear all cached images for this gallery?</string>
<string name="image_cache_cleared">Image cache cleared</string>
<string name="rate_successfully">Rate successfully</string>
<string name="rate_failed">Rate failed</string>
<string name="no_torrents">No torrents</string>
@@ -423,6 +420,8 @@
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
<string name="settings_advanced_read_cache_size">Read cache size</string>
<string name="settings_advanced_app_language_title">App language</string>
<string name="settings_advanced_enable_quic">[Experimental] QUIC support</string>
<string name="settings_advanced_enable_quic_summary">Enable QUIC protocol support (App restart required)</string>
<string name="settings_advanced_export_data">Export data</string>
<string name="settings_advanced_export_data_summary">Save data like download list, quick search list, to external storage</string>
<string name="settings_advanced_export_data_to">Exported data to %s</string>
@@ -468,7 +467,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloud Favorites</string>
<string name="local_favorites">Local Favorites</string>
<string name="search_bar_hint">Search %s</string>
<string name="favorites_search_bar_hint">Search %s</string>
<string name="favorites_title">%s</string>
<string name="favorites_title_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Delete from favorites</string>
@@ -477,7 +476,7 @@
<string name="default_favorites_collection">Default favorites collection</string>
<string name="default_favorites_warning">You won\'t be able to add favorite notes if you enable this</string>
<string name="let_me_select">Let me select</string>
<string name="favorite_note">Favorite Note</string>
<string name="favorite_note">Note</string>
<string name="collections">Collections</string>
<string name="need_sign_in">Signing in is required</string>
<!-- Progress Scene -->
@@ -541,7 +540,6 @@
<string name="download_service">Download Service</string>
<string name="favorite_name">Favorite</string>
<string name="black_dark_theme">Black dark theme</string>
<string name="harmonize_category_color">Harmonize category color for Dynamic Color</string>
<string name="download_filter">Filter</string>
<string name="download_all">All</string>
<string name="download_start_all_reversed">Start all (reversed)</string>
+1 -1
View File
@@ -12,7 +12,7 @@ jnix = { path = "jnix" }
jnix-macros = "0.4"
catch_panic = { path = "catch_panic" }
catch_panic_macros = { path = "catch_panic_macros" }
jni_fn = "0.1.2"
jni_fn = { path = "jni_fn" }
quick-xml = { default-features = false, version = "0.30" }
tl = "0.7"
log = "0.4"
@@ -19,4 +19,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
syn = { version = "1.0", features = ["full"] }
+25 -13
View File
@@ -3,7 +3,10 @@ use std::ops::Deref;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Path};
use syn::{
parse_macro_input, AttributeArgs, Expr, FnArg, ItemFn, Lit, Meta, MetaNameValue, NestedMeta,
Pat, Path,
};
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
const FIRST_ARG_MSG: &str =
@@ -30,21 +33,30 @@ pub fn catch_panic(attr: TokenStream, item: TokenStream) -> TokenStream {
let item = proc_macro2::TokenStream::from(item);
// parse macro arguments
let args = parse_macro_input!(attr as AttributeArgs);
let mut default_value = None;
let mut handler = None;
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("default") {
default_value = Some(meta.value()?.parse::<LitStr>()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("handler") {
handler = Some(meta.value()?.parse::<LitStr>()?.parse::<Path>()?);
Ok(())
} else {
Err(meta.error("unsupported argument"))
}
});
parse_macro_input!(attr with parser);
for arg in args {
if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(str),
..
})) = arg
{
match path.get_ident() {
Some(id) if id == "default" => match str.parse::<Expr>() {
Ok(expr) => default_value = Some(expr),
Err(err) => abort!(err.to_compile_error(), item),
},
Some(id) if id == "handler" => match str.parse::<Path>() {
Ok(expr) => handler = Some(expr),
Err(err) => abort!(err.to_compile_error(), item),
},
_ => {}
}
}
}
let default_value = match default_value {
Some(val) => val.to_token_stream(),
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "jni_fn"
version = "0.1.0"
authors = ["Anton Lazarev <https://antonok.com>"]
edition = "2021"
description = "JNI-compatible method signature generator"
repository = "https://gitlab.com/antonok/jni_fn"
readme = "README.md"
keywords = ["jni", "java", "jvm", "android", "ndk"]
categories = ["api-bindings", "development-tools::procedural-macro-helpers"]
license = "MIT OR Apache-2.0"
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
[dev-dependencies]
jni = "0.20"
+398
View File
@@ -0,0 +1,398 @@
//! JNI-compatible method signature generator for Rust libraries.
//!
//! This crate was designed for use with the [`jni`](https://crates.io/crates/jni) crate, which
//! exposes JNI-compatible type bindings. Although it's possible to use `jni` without `jni_fn`, the
//! procedural macro defined here will make it easier to write the method signatures correctly.
//!
//! See the `jni_fn` attribute macro documentation below for more info and usage examples.
#![deny(missing_docs)]
#![deny(unsafe_code)]
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::spanned::Spanned;
/// Annotate a function with this procedural macro attribute to expose it over the JNI.
///
/// This attribute takes a single string literal as an argument, specifying the package namespace
/// this function should be placed under.
///
/// ```
/// use jni::{ JNIEnv, objects::{ JClass, JString }, sys::jstring };
/// use jni_fn::jni_fn;
///
/// #[jni_fn("com.example.RustBindings")]
/// pub fn sayHello(env: JNIEnv, _: JClass, name: JString) -> jstring {
/// let name_javastr = env.get_string(name).unwrap();
/// let name = name_javastr.to_str().unwrap();
///
/// env.new_string(format!("Hello, {}!", name))
/// .expect("Couldn't create java string!")
/// .into_inner()
/// }
/// ```
///
/// The `sayHello` function will automatically be expanded to have the correct ABI specification
/// and the appropriate JNI-compatible name, i.e. in this case -
/// `Java_com_example_RustBindings_sayHello`.
///
/// Then it can be accessed by, for example, Kotlin code as follows:
/// ```kotlin
/// package com.example.RustBindings
///
/// class RustBindings {
/// private external fun sayHello(name: String): String
///
/// fun greetWorld() {
/// println(sayHello("world"))
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn jni_fn(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
jni_fn2(attr.into(), item.into()).into()
}
/// Deals exclusively with `proc_macro2::TokenStream` instead of `proc_macro::TokenStream`,
/// allowing it and all interior functionality to be unit tested.
fn jni_fn2(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_span = attr.span();
let item_span = item.span();
let mut function: syn::ItemFn = match syn::parse2(item) {
Ok(f) => f,
Err(_e) => {
return syn::Error::new(
item_span,
"The `jni_fn` attribute can only be applied to `fn` items",
)
.to_compile_error()
}
};
let namespace = match syn::parse2::<syn::LitStr>(attr) {
Ok(n) => n,
Err(_e) => return syn::Error::new(attr_span, "The `jni_fn` attribute must have a single string literal supplied to specify the namespace").to_compile_error(),
}.value();
if !valid_namespace(&namespace) {
return syn::Error::new(
attr_span,
"Invalid package namespace supplied to `jni_fn` attribute",
)
.to_compile_error();
}
let orig_fn_name = function.sig.ident.to_string();
function.sig.ident = syn::Ident::new(
&create_jni_fn_name(&namespace, &orig_fn_name),
function.sig.ident.span(),
);
function.attrs.push(syn::Attribute {
pound_token: Default::default(),
style: syn::AttrStyle::Outer,
bracket_token: Default::default(),
path: syn::parse_str("no_mangle").unwrap(),
tokens: TokenStream::new(),
});
function.attrs.push(syn::Attribute {
pound_token: Default::default(),
style: syn::AttrStyle::Outer,
bracket_token: Default::default(),
path: syn::parse_str("allow").unwrap(),
tokens: quote::quote! { (non_snake_case) },
});
if function.sig.abi.is_some() {
return syn::Error::new(function.sig.abi.span(), "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically").to_compile_error();
}
function.sig.abi = Some(syn::Abi {
extern_token: Default::default(),
name: Some(syn::LitStr::new("system", function.sig.ident.span())),
});
if !matches!(function.vis, syn::Visibility::Public(_)) {
return syn::Error::new(
function.vis.span(),
"`jni_fn` attributed functions must have public visibility (`pub`)",
)
.to_compile_error();
}
function.into_token_stream()
}
/// Ensures that `namespace` appears roughly like a valid package name.
///
/// A package name is a '.'-separated identifier list.
///
/// Identifiers are described in section 3.8 of the Java language specification, although some
/// JVM-compatible languages have slightly different restrictions on what is considered a valid
/// identifier. This function attempts to catch obviously incorrect strings.
///
/// Please submit an issue report or patch to make this more permissive if it's required for
/// valid JVM code! Otherwise, making it more restrictive is appreciated as long as it's confirmed
/// to work with multiple JVM-compatible languages.
fn valid_namespace(namespace: &str) -> bool {
/// These shouldn't occur _anywhere_ in the package name.
const FORBIDDEN_CHARS: &[char] = &[
' ', ',', ':', ';', '|', '\\', '/', '!', '@', '#', '%', '^', '&', '*', '(', ')', '{', '}',
'[', ']', '-', '`', '~', '\t', '\n', '\r',
];
for c in FORBIDDEN_CHARS {
if namespace.contains(*c) {
return false;
}
}
fn is_valid_ident(ident: &str) -> bool {
/// These shouldn't occur as the first character of an identifier.
const FORBIDDEN_START_CHARS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
if ident.is_empty() {
return false;
}
for c in FORBIDDEN_START_CHARS {
if ident.starts_with(*c) {
return false;
}
}
true
}
for ident in namespace.split('.') {
if !is_valid_ident(ident) {
return false;
}
}
true
}
/// Creates a JNI-compatible function name from the given namespace and function name.
/// This does _not_ transform the provided function name into `snakeCase` if it's not already; but
/// `#[allow(non_snake_case)]` should be added to prevent errors.
///
/// Importantly, any underscores in the original namespace or function name need to be replaced by
/// "_1", and then dot separators need to be turned into underscores.
fn create_jni_fn_name(namespace: &str, fn_name: &str) -> String {
let namespace_underscored = namespace.replace('_', "_1").replace('.', "_");
let fn_name_underscored = fn_name.replace('_', "_1");
format!("Java_{}_{}", namespace_underscored, fn_name_underscored)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_jni_fn_name() {
assert_eq!(
create_jni_fn_name("com.example.Foo", "init"),
"Java_com_example_Foo_init"
);
assert_eq!(
create_jni_fn_name("com.example.Bar", "closeIt"),
"Java_com_example_Bar_closeIt"
);
assert_eq!(
create_jni_fn_name("com.example.Bar", "close_it"),
"Java_com_example_Bar_close_1it"
);
assert_eq!(
create_jni_fn_name(
"org.signal.client.internal.Native",
"IdentityKeyPair_Deserialize"
),
"Java_org_signal_client_internal_Native_IdentityKeyPair_1Deserialize"
);
}
#[test]
fn test_valid_namespace() {
assert!(valid_namespace("com.example.Foo"));
assert!(valid_namespace("com.antonok.kb"));
assert!(valid_namespace("org.signal.client.internal.Native"));
assert!(valid_namespace("net.under_score"));
assert!(!valid_namespace("com example Foo"));
assert!(!valid_namespace(" com.example.Foo"));
assert!(!valid_namespace("com.example.Foo "));
assert!(!valid_namespace("com.example.1Foo"));
}
#[test]
fn test_code_generation() {
let attr = quote::quote! {
"com.example.Bar"
};
let source = quote::quote! {
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
}
)
);
}
#[test]
fn test_unsafe_fn() {
let attr = quote::quote! {
"com.example.Bar"
};
let source = quote::quote! {
pub unsafe fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
}
)
);
}
#[test]
fn test_non_function() {
let attr = quote::quote! { "com.example.Foo" };
let source = quote::quote! {
enum NotAFunction {
Variant1,
Variant2(u8),
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
compile_error! { "The `jni_fn` attribute can only be applied to `fn` items" }
}
)
);
}
#[test]
fn test_empty_attribute() {
let attr = quote::quote! {};
let source = quote::quote! {
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
compile_error! { "The `jni_fn` attribute must have a single string literal supplied to specify the namespace" }
}
)
);
}
#[test]
fn test_invalid_namespace() {
let attr = quote::quote! { "." };
let source = quote::quote! {
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
compile_error! { "Invalid package namespace supplied to `jni_fn` attribute" }
}
)
);
}
#[test]
fn test_specified_abi() {
let attr = quote::quote! { "com.example.Foo" };
let source = quote::quote! {
pub extern "C" fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
compile_error! { "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically" }
}
)
);
}
#[test]
fn test_wrong_visibility() {
let attr = quote::quote! { "com.example.Foo" };
let source = quote::quote! {
fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
unimplemented!()
}
};
let expanded = jni_fn2(attr.into(), source.into());
assert_eq!(
format!("{}", expanded),
format!(
"{}",
quote::quote! {
compile_error! { "`jni_fn` attributed functions must have public visibility (`pub`)" }
}
)
);
}
}
+11 -15
View File
@@ -30,21 +30,17 @@ pub fn parseTorrent(env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jin
parse_bytebuffer(&mut env, buffer, limit, |dom, parser, _env| {
Some(dom.query_selector("table")?.filter_map(|e| {
let html = e.get(parser)?.inner_html(parser);
if html.contains("Expunged") {
None
} else {
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]iB)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html).unwrap();
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
}
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]B)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html)?;
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
}).collect::<Vec<Torrent>>())
}).unwrap().into_java(&env).forget().into_raw()
}
+5 -5
View File
@@ -1,8 +1,8 @@
plugins {
id("com.android.application") version "8.1.1" apply false
kotlin("android") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" apply false
id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
id("com.android.application") version "8.1.0" apply false
kotlin("android") version "1.9.0" apply false
kotlin("plugin.serialization") version "1.9.0" apply false
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
id("com.mikepenz.aboutlibraries.plugin") version "10.8.3" apply false
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3" apply false
id("dev.shreyaspatil.compose-compiler-report-generator") version "1.1.0" apply false
@@ -13,7 +13,7 @@ allprojects {
}
tasks.register("Delete", Delete::class) {
delete(rootProject.layout.buildDirectory)
delete(rootProject.buildDir)
}
buildscript {
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "EhViewer"
rootProject.name = "Ehviewer"
include(":app")