Compare commits
110
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea75d44e3 | ||
|
|
80481f1778 | ||
|
|
3ea7ff8202 | ||
|
|
b5979c0528 | ||
|
|
f0f79a52b8 | ||
|
|
b549acc1de | ||
|
|
2505cc5d00 | ||
|
|
d5af66b3fc | ||
|
|
f680dc7e46 | ||
|
|
ffd8bbb1ce | ||
|
|
216a043094 | ||
|
|
021eb5f828 | ||
|
|
f3bf8849e9 | ||
|
|
128f7100d7 | ||
|
|
2cf53dd00f | ||
|
|
4a674e46e6 | ||
|
|
f6b0134fdb | ||
|
|
675a6b863e | ||
|
|
b570eeef93 | ||
|
|
71a8b4e607 | ||
|
|
89b866b0fc | ||
|
|
bcce20d9ac | ||
|
|
420e760fb3 | ||
|
|
a168c95d79 | ||
|
|
d8ccec45f9 | ||
|
|
1316875602 | ||
|
|
a85387c7c8 | ||
|
|
d357e83d06 | ||
|
|
51546f6fea | ||
|
|
b4aa5ec593 | ||
|
|
909380f592 | ||
|
|
d5f26643bf | ||
|
|
a15dc4afdf | ||
|
|
b317bc334e | ||
|
|
d0a8d08b26 | ||
|
|
f73a922a8e | ||
|
|
c6682ce718 | ||
|
|
764d1137ae | ||
|
|
02fc972a79 | ||
|
|
7fc727cf2e | ||
|
|
d77e1e6820 | ||
|
|
241d9daae9 | ||
|
|
462e0257a9 | ||
|
|
66fbc7e821 | ||
|
|
3c37f22027 | ||
|
|
0d24bc6602 | ||
|
|
5a50df8179 | ||
|
|
f1251fd150 | ||
|
|
16202efc7b | ||
|
|
26c6052286 | ||
|
|
7ce386a7b8 | ||
|
|
e43cd0a039 | ||
|
|
70be405032 | ||
|
|
9f3ad8142a | ||
|
|
51d9ece30c | ||
|
|
d8e79f879f | ||
|
|
76d50b56ee | ||
|
|
c8d1e957a1 | ||
|
|
b287af4824 | ||
|
|
416ef8d6d7 | ||
|
|
7c71abb985 | ||
|
|
900f2462a7 | ||
|
|
dd982c6cb9 | ||
|
|
927a011c9d | ||
|
|
ad0ad47a40 | ||
|
|
71a424c15d | ||
|
|
145b7049a8 | ||
|
|
323d258bc2 | ||
|
|
56ba52d7b9 | ||
|
|
54bd6f6ed8 | ||
|
|
f06167ba4b | ||
|
|
60438ae1ed | ||
|
|
1b87750b87 | ||
|
|
40cdc566c8 | ||
|
|
428ae07e7f | ||
|
|
481ad22b53 | ||
|
|
65f58b93ed | ||
|
|
45411fa14d | ||
|
|
8ba1ba4fb1 | ||
|
|
8427e1b3a5 | ||
|
|
32f0f2a0cf | ||
|
|
8b94997cde | ||
|
|
bd2d54f774 | ||
|
|
34be285510 | ||
|
|
7b906f3c3e | ||
|
|
82ba22cda2 | ||
|
|
b1a9601ae9 | ||
|
|
7c1a297a99 | ||
|
|
a8195c0e7c | ||
|
|
08713e3922 | ||
|
|
0485d52e3e | ||
|
|
8b1723e19b | ||
|
|
9cb683dc55 | ||
|
|
dce1589c19 | ||
|
|
107cf16d36 | ||
|
|
2bab97c829 | ||
|
|
5242d825c2 | ||
|
|
11d3799b03 | ||
|
|
55c7f6b896 | ||
|
|
a09d607177 | ||
|
|
626a101208 | ||
|
|
f4ca3acbf6 | ||
|
|
86835a3bd7 | ||
|
|
0ef46e2e11 | ||
|
|
04878447b7 | ||
|
|
3669d5d90e | ||
|
|
ee5e222a13 | ||
|
|
9ea44ff59a | ||
|
|
0474e2c0f5 | ||
|
|
4a909d4b93 |
No files matched your search
@@ -56,3 +56,10 @@ jobs:
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
|
||||
- name: Update mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release/mapping.txt
|
||||
retention-days: 5
|
||||
@@ -49,6 +49,7 @@ jobs:
|
||||
app/build/outputs/apk/release/EhViewer-x86_64.apk
|
||||
app/build/outputs/apk/release/EhViewer-x86.apk
|
||||
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
|
||||
app/build/outputs/mapping/release/mapping.txt
|
||||
|
||||
- name: Collect Apks
|
||||
run: |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
<img src="art/launcher_icon-web.svg" width="200">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
|
||||
<br>EhViewer<br>
|
||||
</h1>
|
||||
|
||||
@@ -66,12 +66,12 @@ with Material Design 3 and Dynamic Color Support
|
||||
|
||||
# Download
|
||||
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="art/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="art/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# Thanks
|
||||
|
||||
@@ -94,7 +94,7 @@ Here is the libraries
|
||||
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022 Tarsin Norbin
|
||||
Copyright 2022-2023 Tarsin Norbin
|
||||
|
||||
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
+4
-8
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 180027
|
||||
versionName "1.8.5.0-preview"
|
||||
versionCode 180030
|
||||
versionName "1.8.5.2"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
}
|
||||
|
||||
@@ -52,10 +52,6 @@ android {
|
||||
freeCompilerArgs += [
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
// Generate smaller bytecode by not generating runtime not-null assertions.
|
||||
"-Xno-call-assertions",
|
||||
"-Xno-param-assertions",
|
||||
"-Xno-receiver-assertions",
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
|
||||
]
|
||||
@@ -112,7 +108,7 @@ dependencies {
|
||||
|
||||
implementation "androidx.annotation:annotation:1.5.0"
|
||||
implementation "androidx.annotation:annotation-experimental:1.3.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.0-rc01"
|
||||
implementation "androidx.appcompat:appcompat:1.7.0-alpha01"
|
||||
|
||||
def arch_core_version = "2.1.0"
|
||||
implementation "androidx.arch.core:core-common:$arch_core_version"
|
||||
@@ -185,7 +181,7 @@ dependencies {
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.8.0-beta01'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
|
||||
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp'
|
||||
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
|
||||
Vendored
+3
-10
@@ -2,22 +2,15 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
|
||||
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
|
||||
|
||||
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
||||
|
||||
-renamesourcefileattribute
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
-mergeinterfacesaggressively
|
||||
-overloadaggressively
|
||||
@@ -14,7 +14,8 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
@@ -32,10 +33,6 @@
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:name="com.hippo.ehviewer.EhApplication"
|
||||
android:allowBackup="true"
|
||||
@@ -45,12 +42,13 @@
|
||||
android:fullBackupContent="@xml/backup_scheme"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.MainActivity"
|
||||
@@ -105,7 +103,6 @@
|
||||
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:colorMode="wideColorGamut"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
@@ -161,6 +158,15 @@
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.hippo.ehviewer.download.DownloadService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
|
||||
@@ -47,4 +47,4 @@ add_subdirectory(libarchive)
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv2 -Wl,--gc-sections)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
|
||||
@@ -43,6 +43,11 @@ typedef struct {
|
||||
struct archive_entry *entry;
|
||||
} archive_ctx;
|
||||
|
||||
typedef struct {
|
||||
const char *filename;
|
||||
int index;
|
||||
} entry;
|
||||
|
||||
pthread_mutex_t ctx_lock;
|
||||
static archive_ctx **ctx_pool;
|
||||
#define CTX_POOL_SIZE 20
|
||||
@@ -51,6 +56,8 @@ static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
static size_t archiveSize = 0;
|
||||
static entry *entries = NULL;
|
||||
static size_t entryCount = 0;
|
||||
|
||||
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
|
||||
|
||||
@@ -77,6 +84,30 @@ static inline int filename_is_playable_file(const char *name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
int a_len = (int) strlen(fa);
|
||||
int b_len = (int) strlen(fb);
|
||||
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname(ctx->entry);
|
||||
if (filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
@@ -221,10 +252,9 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
if (r) {
|
||||
if (r == -ENOMEM)
|
||||
LOGE("%s", "Mem alloc failed");
|
||||
r = 0;
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
r = archive_list_all_entries(ctx);
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
@@ -251,6 +281,19 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
}
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
|
||||
r = archive_alloc_ctx(&ctx);
|
||||
if (r) {
|
||||
if (r == -ENOMEM)
|
||||
LOGE("%s", "Mem alloc failed");
|
||||
r = 0;
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
return r;
|
||||
}
|
||||
@@ -259,6 +302,7 @@ JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
@@ -295,6 +339,10 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
passwd = NULL;
|
||||
need_encrypt = false;
|
||||
munmap(archiveAddr, archiveSize);
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@@ -338,6 +386,7 @@ JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
@@ -352,6 +401,7 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
@@ -363,6 +413,7 @@ Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint in
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
free(addr);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package androidx.swiperefreshlayout.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : SwipeRefreshLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
val surfaceColor =
|
||||
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
|
||||
val overlayColor =
|
||||
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
|
||||
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
|
||||
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
|
||||
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
|
||||
}
|
||||
}
|
||||
@@ -476,8 +476,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private fun setGestureDetector(context: Context) {
|
||||
detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
||||
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
|
||||
// The e1 can be null, that's weird and cause crash
|
||||
if (panEnabled && isReady && vTranslate != null && e1 != null && e2 != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
|
||||
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
|
||||
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
|
||||
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
|
||||
|
||||
@@ -29,8 +29,6 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
|
||||
private external fun openArchive(fd: Int, size: Long): Int
|
||||
external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
|
||||
external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
external fun extractToFd(index: Int, fd: Int)
|
||||
external fun getFilename(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
@@ -40,4 +38,9 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.util.HashCodeUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -85,32 +86,13 @@ public class SimpleDiskCache {
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(digest.digest());
|
||||
cacheKey = HashCodeUtils.bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
private static String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
public class BatteryDrawable extends Drawable {
|
||||
|
||||
public static final int WARN_LIMIT = 15;
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = BatteryDrawable.class.getSimpleName();
|
||||
private final Paint mPaint;
|
||||
private final Rect mTopRect;
|
||||
private final Rect mBottomRect;
|
||||
private final Rect mRightRect;
|
||||
private final Rect mHeadRect;
|
||||
private final Rect mElectRect;
|
||||
private int mColor = Color.WHITE;
|
||||
private int mWarningColor = Color.RED;
|
||||
private int mElect = -1;
|
||||
private int mStart;
|
||||
private int mStop;
|
||||
|
||||
public BatteryDrawable() {
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
mTopRect = new Rect();
|
||||
mBottomRect = new Rect();
|
||||
mRightRect = new Rect();
|
||||
mHeadRect = new Rect();
|
||||
mElectRect = new Rect();
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
int width = bounds.width();
|
||||
int height = bounds.height();
|
||||
int strokeWidth = (int) (Math.sqrt(width * width + height * height) * 0.06f);
|
||||
int turn1 = width * 6 / 7;
|
||||
int turn2 = height / 3;
|
||||
int secBottom = height - strokeWidth;
|
||||
mStart = strokeWidth;
|
||||
mStop = turn1 - strokeWidth;
|
||||
|
||||
mTopRect.set(0, 0, turn1, strokeWidth);
|
||||
mBottomRect.set(0, secBottom, turn1, height);
|
||||
mRightRect.set(turn1 - strokeWidth, strokeWidth, turn1, secBottom);
|
||||
mHeadRect.set(turn1, turn2, width, height - turn2);
|
||||
mElectRect.set(0, strokeWidth, mStop, secBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* How to draw:<br>
|
||||
* |------------------------------|<br>
|
||||
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
|
||||
* |------------------------------|---|<br>
|
||||
* |/////////////////| |//|\\\|<br>
|
||||
* |/////////////////| |//|\\\|<br>
|
||||
* |------------------------------|---|<br>
|
||||
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
|
||||
* |------------------------------|
|
||||
*/
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (mElect == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f);
|
||||
|
||||
canvas.drawRect(mTopRect, mPaint);
|
||||
canvas.drawRect(mBottomRect, mPaint);
|
||||
canvas.drawRect(mRightRect, mPaint);
|
||||
canvas.drawRect(mHeadRect, mPaint);
|
||||
canvas.drawRect(mElectRect, mPaint);
|
||||
}
|
||||
|
||||
private boolean isWarn() {
|
||||
return mElect <= WARN_LIMIT;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
if (mColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
mColor = color;
|
||||
if (!isWarn()) {
|
||||
mPaint.setColor(mColor);
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public void setWarningColor(int color) {
|
||||
if (mWarningColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWarningColor = color;
|
||||
if (isWarn()) {
|
||||
mPaint.setColor(mWarningColor);
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public void setElect(int elect) {
|
||||
if (mElect == elect) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElect = elect;
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
public void setElect(int elect, boolean warn) {
|
||||
if (mElect == elect) {
|
||||
return;
|
||||
}
|
||||
|
||||
mElect = elect;
|
||||
updatePaint(warn);
|
||||
}
|
||||
|
||||
private void updatePaint() {
|
||||
updatePaint(isWarn());
|
||||
}
|
||||
|
||||
private void updatePaint(boolean warn) {
|
||||
if (warn) {
|
||||
mPaint.setColor(mWarningColor);
|
||||
} else {
|
||||
mPaint.setColor(mColor);
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
mPaint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.os.Build
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
@@ -28,21 +27,21 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.LruCache
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.Native
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.beerbelly.SimpleDiskCache
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhDns
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhSSLSocketFactory
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhX509TrustManager
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
@@ -57,6 +56,8 @@ import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IntIdGenerator
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -77,7 +78,6 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
private val mIdGenerator = IntIdGenerator()
|
||||
private val mGlobalStuffMap = HashMap<Int, Any>()
|
||||
private val mActivityList = ArrayList<Activity>()
|
||||
private var initialized = false
|
||||
val topActivity: EhActivity?
|
||||
get() = if (mActivityList.isNotEmpty()) {
|
||||
mActivityList[mActivityList.size - 1] as EhActivity
|
||||
@@ -107,17 +107,15 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
super<SceneApplication>.onCreate()
|
||||
Native.initialize()
|
||||
GetText.initialize(this)
|
||||
Settings.initialize(this)
|
||||
Settings.initialize()
|
||||
ReadableTime.initialize(this)
|
||||
AppConfig.initialize(this)
|
||||
SpiderDen.initialize(this)
|
||||
EhDB.initialize(this)
|
||||
EhEngine.initialize()
|
||||
BitmapUtils.initialize(this)
|
||||
EhTagDatabase.update()
|
||||
|
||||
// Locales can be managed by system automatically above Snow Cone v2
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()))
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
|
||||
|
||||
GlobalScope.launch {
|
||||
@@ -147,7 +145,6 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
theDawnOfNewDay()
|
||||
}
|
||||
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
|
||||
initialized = true
|
||||
}
|
||||
|
||||
private suspend fun theDawnOfNewDay() {
|
||||
@@ -296,7 +293,7 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(ehCookieStore)
|
||||
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
|
||||
.dns(EhDns())
|
||||
.dns(EhDns)
|
||||
.proxySelector(ehProxySelector)
|
||||
|
||||
if (Settings.getDF()) {
|
||||
@@ -315,10 +312,10 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
trustManager = trustManagers[0] as X509TrustManager
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
trustManager = EhX509TrustManager()
|
||||
trustManager = EhX509TrustManager
|
||||
}
|
||||
|
||||
builder.sslSocketFactory(EhSSLSocketFactory(), trustManager)
|
||||
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
|
||||
builder.proxy(Proxy.NO_PROXY)
|
||||
}
|
||||
builder.build()
|
||||
@@ -335,19 +332,20 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
|
||||
|
||||
@JvmStatic
|
||||
val spiderInfoCache by lazy {
|
||||
SimpleDiskCache(
|
||||
File(application.cacheDir, "spider_info"),
|
||||
20 * 1024 * 1024
|
||||
)
|
||||
DiskCache.Builder().directory(File(application.cacheDir, "spider_info"))
|
||||
.maxSizeBytes(20 * 1024 * 1024).build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val downloadManager by lazy { DownloadManager(application) }
|
||||
val downloadManager by lazy { DownloadManager() }
|
||||
|
||||
@JvmStatic
|
||||
val hosts by lazy { Hosts(application, "hosts.db") }
|
||||
|
||||
@JvmStatic
|
||||
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
|
||||
|
||||
@JvmStatic
|
||||
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
@@ -86,7 +85,6 @@ public class Settings {
|
||||
public static final int DEFAULT_READ_CACHE_SIZE = 320;
|
||||
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
|
||||
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||
public static final String KEY_APP_LANGUAGE = "app_language";
|
||||
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
|
||||
@@ -124,34 +122,6 @@ public class Settings {
|
||||
private static final boolean DEFAULT_METERED_NETWORK_WARNING = false;
|
||||
private static final String KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip";
|
||||
private static final boolean DEFAULT_APP_LINK_VERIFY_TIP = false;
|
||||
/********************
|
||||
****** Read
|
||||
********************/
|
||||
private static final String KEY_SCREEN_ROTATION = "screen_rotation";
|
||||
private static final int DEFAULT_SCREEN_ROTATION = 0;
|
||||
private static final String KEY_READING_DIRECTION = "reading_direction";
|
||||
private static final String KEY_PAGE_SCALING = "page_scaling";
|
||||
private static final String KEY_START_POSITION = "start_position";
|
||||
private static final String KEY_KEEP_SCREEN_ON = "keep_screen_on";
|
||||
private static final boolean DEFAULT_KEEP_SCREEN_ON = false;
|
||||
private static final String KEY_SHOW_CLOCK = "gallery_show_clock";
|
||||
private static final boolean DEFAULT_SHOW_CLOCK = true;
|
||||
private static final String KEY_SHOW_PROGRESS = "gallery_show_progress";
|
||||
private static final boolean DEFAULT_SHOW_PROGRESS = true;
|
||||
private static final String KEY_SHOW_BATTERY = "gallery_show_battery";
|
||||
private static final boolean DEFAULT_SHOW_BATTERY = true;
|
||||
private static final String KEY_SHOW_PAGE_INTERVAL = "gallery_show_page_interval";
|
||||
private static final boolean DEFAULT_SHOW_PAGE_INTERVAL = true;
|
||||
private static final String KEY_VOLUME_PAGE = "volume_page";
|
||||
private static final boolean DEFAULT_VOLUME_PAGE = false;
|
||||
private static final String KEY_REVERSE_VOLUME_PAGE = "reserve_volume_page";
|
||||
private static final boolean DEFAULT_REVERSE_VOLUME_PAGE = false;
|
||||
private static final String KEY_READING_FULLSCREEN = "reading_fullscreen";
|
||||
private static final boolean VALUE_READING_FULLSCREEN = true;
|
||||
private static final String KEY_CUSTOM_SCREEN_LIGHTNESS = "custom_screen_lightness";
|
||||
private static final boolean DEFAULT_CUSTOM_SCREEN_LIGHTNESS = false;
|
||||
private static final String KEY_SCREEN_LIGHTNESS = "screen_lightness";
|
||||
private static final int DEFAULT_SCREEN_LIGHTNESS = 50;
|
||||
private static final boolean DEFAULT_MEDIA_SCAN = false;
|
||||
private static final String KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label";
|
||||
private static final String DEFAULT_RECENT_DOWNLOAD_LABEL = null;
|
||||
@@ -165,8 +135,6 @@ public class Settings {
|
||||
private static final int DEFAULT_PRELOAD_IMAGE = 5;
|
||||
private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image";
|
||||
private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false;
|
||||
private static final String KEY_READ_THEME = "read_theme";
|
||||
private static final int DEFAULT_READ_THEME = 1;
|
||||
/********************
|
||||
****** Favorites
|
||||
********************/
|
||||
@@ -213,15 +181,12 @@ public class Settings {
|
||||
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
|
||||
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
|
||||
private static final boolean DEFAULT_FRONTING = false;
|
||||
private static final String DEFAULT_APP_LANGUAGE = "system";
|
||||
private static final String KEY_PROXY_TYPE = "proxy_type";
|
||||
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
|
||||
private static final String KEY_PROXY_IP = "proxy_ip";
|
||||
private static final String DEFAULT_PROXY_IP = null;
|
||||
private static final String KEY_PROXY_PORT = "proxy_port";
|
||||
private static final int DEFAULT_PROXY_PORT = -1;
|
||||
private static final String KEY_GUIDE_GALLERY = "guide_gallery";
|
||||
private static final boolean DEFAULT_GUIDE_GALLERY = true;
|
||||
private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code";
|
||||
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
|
||||
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
|
||||
@@ -229,15 +194,15 @@ public class Settings {
|
||||
private static final String KEY_REQUEST_NEWS = "request_news";
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = true;
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
|
||||
public static boolean LIST_THUMB_SIZE_INITED = false;
|
||||
public static SharedPreferences sSettingsPre;
|
||||
private static int LIST_THUMB_SIZE = 40;
|
||||
private static Context sContext;
|
||||
private static EhConfig sEhConfig;
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sContext = context.getApplicationContext();
|
||||
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(sContext);
|
||||
public static void initialize() {
|
||||
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(EhApplication.getApplication());
|
||||
sEhConfig = loadEhConfig();
|
||||
fixDefaultValue();
|
||||
}
|
||||
@@ -259,15 +224,6 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
public static Locale getLocale() {
|
||||
String language = Settings.getAppLanguage();
|
||||
if (language != null && !language.equals("system")) {
|
||||
return Locale.forLanguageTag(language);
|
||||
} else {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private static EhConfig loadEhConfig() {
|
||||
EhConfig ehConfig = new EhConfig();
|
||||
ehConfig.imageSize = getImageResolution();
|
||||
@@ -431,7 +387,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static int dip2px(int dpValue) {
|
||||
final float scale = sContext.getResources().getDisplayMetrics().density;
|
||||
final float scale = EhApplication.getApplication().getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
@@ -495,114 +451,6 @@ public class Settings {
|
||||
putBoolean(KEY_APP_LINK_VERIFY_TIP, value);
|
||||
}
|
||||
|
||||
public static int getScreenRotation() {
|
||||
return getIntFromStr(KEY_SCREEN_ROTATION, DEFAULT_SCREEN_ROTATION);
|
||||
}
|
||||
|
||||
public static void putScreenRotation(int value) {
|
||||
putIntToStr(KEY_SCREEN_ROTATION, value);
|
||||
}
|
||||
|
||||
public static void putReadingDirection(int value) {
|
||||
putIntToStr(KEY_READING_DIRECTION, value);
|
||||
}
|
||||
|
||||
public static void putPageScaling(int value) {
|
||||
putIntToStr(KEY_PAGE_SCALING, value);
|
||||
}
|
||||
|
||||
public static void putStartPosition(int value) {
|
||||
putIntToStr(KEY_START_POSITION, value);
|
||||
}
|
||||
|
||||
public static boolean getKeepScreenOn() {
|
||||
return getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
public static void putKeepScreenOn(boolean value) {
|
||||
putBoolean(KEY_KEEP_SCREEN_ON, value);
|
||||
}
|
||||
|
||||
public static boolean getShowClock() {
|
||||
return getBoolean(KEY_SHOW_CLOCK, DEFAULT_SHOW_CLOCK);
|
||||
}
|
||||
|
||||
public static void putShowClock(boolean value) {
|
||||
putBoolean(KEY_SHOW_CLOCK, value);
|
||||
}
|
||||
|
||||
public static boolean getShowProgress() {
|
||||
return getBoolean(KEY_SHOW_PROGRESS, DEFAULT_SHOW_PROGRESS);
|
||||
}
|
||||
|
||||
public static void putShowProgress(boolean value) {
|
||||
putBoolean(KEY_SHOW_PROGRESS, value);
|
||||
}
|
||||
|
||||
public static boolean getShowBattery() {
|
||||
return getBoolean(KEY_SHOW_BATTERY, DEFAULT_SHOW_BATTERY);
|
||||
}
|
||||
|
||||
public static void putShowBattery(boolean value) {
|
||||
putBoolean(KEY_SHOW_BATTERY, value);
|
||||
}
|
||||
|
||||
public static boolean getShowPageInterval() {
|
||||
return getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL);
|
||||
}
|
||||
|
||||
public static void putShowPageInterval(boolean value) {
|
||||
putBoolean(KEY_SHOW_PAGE_INTERVAL, value);
|
||||
}
|
||||
|
||||
public static boolean getVolumePage() {
|
||||
return getBoolean(KEY_VOLUME_PAGE, DEFAULT_VOLUME_PAGE);
|
||||
}
|
||||
|
||||
public static void putVolumePage(boolean value) {
|
||||
putBoolean(KEY_VOLUME_PAGE, value);
|
||||
}
|
||||
|
||||
public static boolean getReverseVolumePage() {
|
||||
return getBoolean(KEY_REVERSE_VOLUME_PAGE, DEFAULT_REVERSE_VOLUME_PAGE);
|
||||
}
|
||||
|
||||
public static void putReverseVolumePage(boolean value) {
|
||||
putBoolean(KEY_REVERSE_VOLUME_PAGE, value);
|
||||
}
|
||||
|
||||
public static boolean getReadingFullscreen() {
|
||||
return getBoolean(KEY_READING_FULLSCREEN, VALUE_READING_FULLSCREEN);
|
||||
}
|
||||
|
||||
public static void putReadingFullscreen(boolean value) {
|
||||
putBoolean(KEY_READING_FULLSCREEN, value);
|
||||
}
|
||||
|
||||
public static boolean getCustomScreenLightness() {
|
||||
return getBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, DEFAULT_CUSTOM_SCREEN_LIGHTNESS);
|
||||
}
|
||||
|
||||
public static void putCustomScreenLightness(boolean value) {
|
||||
putBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, value);
|
||||
}
|
||||
|
||||
public static int getScreenLightness() {
|
||||
return getInt(KEY_SCREEN_LIGHTNESS, DEFAULT_SCREEN_LIGHTNESS);
|
||||
}
|
||||
|
||||
public static void putScreenLightness(int value) {
|
||||
putInt(KEY_SCREEN_LIGHTNESS, value);
|
||||
}
|
||||
|
||||
public static int getReadTheme() {
|
||||
return getIntFromStr(KEY_READ_THEME, DEFAULT_READ_THEME);
|
||||
}
|
||||
|
||||
public static void putReadTheme(int value) {
|
||||
putIntToStr(KEY_READ_THEME, value);
|
||||
}
|
||||
|
||||
public static boolean getEnabledSecurity() {
|
||||
return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY);
|
||||
}
|
||||
@@ -616,7 +464,7 @@ public class Settings {
|
||||
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
|
||||
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
|
||||
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
|
||||
dir = UniFile.fromUri(sContext, builder.build());
|
||||
dir = UniFile.fromUri(EhApplication.getApplication(), builder.build());
|
||||
return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir());
|
||||
}
|
||||
|
||||
@@ -783,6 +631,14 @@ public class Settings {
|
||||
putInt(KEY_DEFAULT_FAV_SLOT, value);
|
||||
}
|
||||
|
||||
public static boolean getQSSaveProgress() {
|
||||
return getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS);
|
||||
}
|
||||
|
||||
public static void putQSSaveProgress(boolean value) {
|
||||
putBoolean(KEY_QS_SAVE_PROGRESS, value);
|
||||
}
|
||||
|
||||
public static boolean getSaveParseErrorBody() {
|
||||
return getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY);
|
||||
}
|
||||
@@ -823,10 +679,6 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static String getAppLanguage() {
|
||||
return getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE);
|
||||
}
|
||||
|
||||
public static int getProxyType() {
|
||||
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
|
||||
}
|
||||
@@ -851,14 +703,6 @@ public class Settings {
|
||||
putInt(KEY_PROXY_PORT, value);
|
||||
}
|
||||
|
||||
public static boolean getGuideGallery() {
|
||||
return getBoolean(KEY_GUIDE_GALLERY, DEFAULT_GUIDE_GALLERY);
|
||||
}
|
||||
|
||||
public static void putGuideGallery(boolean value) {
|
||||
putBoolean(KEY_GUIDE_GALLERY, value);
|
||||
}
|
||||
|
||||
public static int getClipboardTextHashCode() {
|
||||
return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import okhttp3.Dns
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class EhDns : Dns {
|
||||
object EhDns : Dns {
|
||||
private val hosts = EhApplication.hosts
|
||||
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.hippo.ehviewer.dao.Filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class EhFilter {
|
||||
|
||||
@@ -31,36 +33,38 @@ public final class EhFilter {
|
||||
public static final int MODE_UPLOADER = 1;
|
||||
public static final int MODE_TAG = 2;
|
||||
public static final int MODE_TAG_NAMESPACE = 3;
|
||||
public static final int MODE_COMMENTER = 4;
|
||||
public static final int MODE_COMMENT = 5;
|
||||
private static final String TAG = EhFilter.class.getSimpleName();
|
||||
private static EhFilter sInstance;
|
||||
private final List<Filter> mTitleFilterList = new ArrayList<>();
|
||||
private final List<Filter> mUploaderFilterList = new ArrayList<>();
|
||||
private final List<Filter> mTagFilterList = new ArrayList<>();
|
||||
private final List<Filter> mTagNamespaceFilterList = new ArrayList<>();
|
||||
private final List<Filter> mCommenterFilterList = new ArrayList<>();
|
||||
private final List<Filter> mCommentFilterList = new ArrayList<>();
|
||||
|
||||
private EhFilter() {
|
||||
List<Filter> list = EhDB.getAllFilter();
|
||||
for (int i = 0, n = list.size(); i < n; i++) {
|
||||
Filter filter = list.get(i);
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
case MODE_TITLE -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTitleFilterList.add(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
}
|
||||
case MODE_TAG -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
}
|
||||
case MODE_TAG_NAMESPACE -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagNamespaceFilterList.add(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
}
|
||||
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
|
||||
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
|
||||
case MODE_COMMENT -> mCommentFilterList.add(filter);
|
||||
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,30 +92,36 @@ public final class EhFilter {
|
||||
return mTagNamespaceFilterList;
|
||||
}
|
||||
|
||||
public List<Filter> getCommenterFilterList() {
|
||||
return mCommenterFilterList;
|
||||
}
|
||||
|
||||
public List<Filter> getCommentFilterList() {
|
||||
return mCommentFilterList;
|
||||
}
|
||||
|
||||
public synchronized boolean addFilter(Filter filter) {
|
||||
// enable filter by default before it is added to database
|
||||
filter.enable = true;
|
||||
if (!EhDB.addFilter(filter)) return false;
|
||||
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
case MODE_TITLE -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTitleFilterList.add(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
}
|
||||
case MODE_TAG -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagFilterList.add(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
}
|
||||
case MODE_TAG_NAMESPACE -> {
|
||||
filter.text = filter.text.toLowerCase();
|
||||
mTagNamespaceFilterList.add(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
}
|
||||
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
|
||||
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
|
||||
case MODE_COMMENT -> mCommentFilterList.add(filter);
|
||||
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -124,21 +134,13 @@ public final class EhFilter {
|
||||
EhDB.deleteFilter(filter);
|
||||
|
||||
switch (filter.mode) {
|
||||
case MODE_TITLE:
|
||||
mTitleFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_UPLOADER:
|
||||
mUploaderFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_TAG:
|
||||
mTagFilterList.remove(filter);
|
||||
break;
|
||||
case MODE_TAG_NAMESPACE:
|
||||
mTagNamespaceFilterList.remove(filter);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
break;
|
||||
case MODE_TITLE -> mTitleFilterList.remove(filter);
|
||||
case MODE_TAG -> mTagFilterList.remove(filter);
|
||||
case MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter);
|
||||
case MODE_UPLOADER -> mUploaderFilterList.remove(filter);
|
||||
case MODE_COMMENTER -> mCommenterFilterList.remove(filter);
|
||||
case MODE_COMMENT -> mCommentFilterList.remove(filter);
|
||||
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,4 +274,40 @@ public final class EhFilter {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean filterCommenter(String commenter) {
|
||||
if (null == commenter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Filter> filters = mCommenterFilterList;
|
||||
if (filters.size() > 0) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable && commenter.equals(filters.get(i).text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean filterComment(String comment) {
|
||||
if (null == comment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Filter> filters = mCommentFilterList;
|
||||
if (filters.size() > 0) {
|
||||
for (int i = 0, n = filters.size(); i < n; i++) {
|
||||
if (filters.get(i).enable) {
|
||||
Pattern p = Pattern.compile(filters.get(i).text);
|
||||
Matcher m = p.matcher(comment);
|
||||
if (m.find()) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import java.net.Socket
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
class EhSSLSocketFactory : SSLSocketFactory() {
|
||||
object EhSSLSocketFactory : SSLSocketFactory() {
|
||||
private val sslSocketFactory = getDefault() as SSLSocketFactory
|
||||
override fun getDefaultCipherSuites(): Array<String> {
|
||||
return sslSocketFactory.defaultCipherSuites
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public class EhTagDatabase {
|
||||
|
||||
private static final Map<String, String> NAMESPACE_TO_PREFIX = new HashMap<>();
|
||||
// TODO more lock for different language
|
||||
private static final Lock lock = new ReentrantLock();
|
||||
private static volatile EhTagDatabase instance;
|
||||
|
||||
static {
|
||||
NAMESPACE_TO_PREFIX.put("artist", "a:");
|
||||
NAMESPACE_TO_PREFIX.put("cosplayer", "cos:");
|
||||
NAMESPACE_TO_PREFIX.put("character", "c:");
|
||||
NAMESPACE_TO_PREFIX.put("female", "f:");
|
||||
NAMESPACE_TO_PREFIX.put("group", "g:");
|
||||
NAMESPACE_TO_PREFIX.put("language", "l:");
|
||||
NAMESPACE_TO_PREFIX.put("male", "m:");
|
||||
NAMESPACE_TO_PREFIX.put("mixed", "x:");
|
||||
NAMESPACE_TO_PREFIX.put("other", "o:");
|
||||
NAMESPACE_TO_PREFIX.put("parody", "p:");
|
||||
NAMESPACE_TO_PREFIX.put("reclass", "r:");
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final byte[] tags;
|
||||
|
||||
public EhTagDatabase(String name, BufferedSource source) throws IOException {
|
||||
this.name = name;
|
||||
String[] tmp;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
source.readInt();
|
||||
for (String i : source.readUtf8().split("\n")) {
|
||||
tmp = i.split("\r", 2);
|
||||
buffer.append(tmp[0]);
|
||||
buffer.append("\r");
|
||||
try {
|
||||
buffer.append(new String(Base64.decode(tmp[1], Base64.DEFAULT), StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
buffer.append(tmp[1]);
|
||||
}
|
||||
buffer.append("\n");
|
||||
}
|
||||
byte[] b = buffer.toString().getBytes(StandardCharsets.UTF_8);
|
||||
int totalBytes = b.length;
|
||||
tags = new byte[totalBytes];
|
||||
System.arraycopy(b, 0, tags, 0, totalBytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EhTagDatabase getInstance(Context context) {
|
||||
if (isPossible(context)) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String namespaceToPrefix(String namespace) {
|
||||
return NAMESPACE_TO_PREFIX.get(namespace);
|
||||
}
|
||||
|
||||
private static String[] getMetadata(Context context) {
|
||||
String[] metadata = context.getResources().getStringArray(R.array.tag_translation_metadata);
|
||||
if (metadata.length == 4) {
|
||||
return metadata;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPossible(Context context) {
|
||||
return getMetadata(context) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileContent(File file, int length) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(file))) {
|
||||
byte[] content = new byte[length];
|
||||
source.readFully(content);
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileSha1(File file) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
int n;
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
while ((n = is.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, n);
|
||||
}
|
||||
return digest.digest();
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean equals(byte[] b1, byte[] b2) {
|
||||
if (b1 == null && b2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (b1 == null || b2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (b1.length != b2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < b1.length; i++) {
|
||||
if (b1[i] != b2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkData(File sha1File, File dataFile) {
|
||||
byte[] s1 = getFileContent(sha1File, 20);
|
||||
if (s1 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] s2 = getFileSha1(dataFile);
|
||||
if (s2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return equals(s1, s2);
|
||||
}
|
||||
|
||||
private static boolean save(OkHttpClient client, String url, File file) {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
Call call = client.newCall(request);
|
||||
try (Response response = call.execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream is = body.byteStream(); OutputStream os = new FileOutputStream(file)) {
|
||||
IOUtils.copy(is, os);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
ExceptionUtils.throwIfFatal(t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void update(Context context) {
|
||||
String[] urls = getMetadata(context);
|
||||
if (urls == null || urls.length != 4) {
|
||||
// Clear tags if it's not possible
|
||||
instance = null;
|
||||
return;
|
||||
}
|
||||
|
||||
String sha1Name = urls[0];
|
||||
String sha1Url = urls[1];
|
||||
String dataName = urls[2];
|
||||
String dataUrl = urls[3];
|
||||
|
||||
// Clear tags if name if different
|
||||
EhTagDatabase tmp = instance;
|
||||
if (tmp != null && !tmp.name.equals(dataName)) {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
if (!lock.tryLock()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dir = AppConfig.getFilesDir("tag-translations");
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current sha1 and current data
|
||||
File sha1File = new File(dir, sha1Name);
|
||||
File dataFile = new File(dir, dataName);
|
||||
if (!checkData(sha1File, dataFile)) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
|
||||
// Read current EhTagDatabase
|
||||
if (instance == null && dataFile.exists()) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
}
|
||||
|
||||
OkHttpClient client = EhApplication.getOkHttpClient();
|
||||
|
||||
// Save new sha1
|
||||
File tempSha1File = new File(dir, sha1Name + ".tmp");
|
||||
if (!save(client, sha1Url, tempSha1File)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and current data
|
||||
if (checkData(tempSha1File, dataFile)) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save new data
|
||||
File tempDataFile = new File(dir, dataName + ".tmp");
|
||||
if (!save(client, dataUrl, tempDataFile)) {
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1File, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
tempSha1File.renameTo(sha1File);
|
||||
tempDataFile.renameTo(dataFile);
|
||||
|
||||
// Read new EhTagDatabase
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTranslation(String tag) {
|
||||
return search(tags, tag.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String search(byte[] tags, byte[] tag) {
|
||||
int low = 0;
|
||||
int high = tags.length;
|
||||
while (low < high) {
|
||||
int start = (low + high) / 2;
|
||||
// Look for the starting '\n'
|
||||
while (start > -1 && tags[start] != '\n') {
|
||||
start--;
|
||||
}
|
||||
start++;
|
||||
|
||||
// Look for the middle '\r'.
|
||||
int middle = 1;
|
||||
while (tags[start + middle] != '\r') {
|
||||
middle++;
|
||||
}
|
||||
|
||||
// Look for the ending '\n'
|
||||
int end = middle + 1;
|
||||
while (tags[start + end] != '\n') {
|
||||
end++;
|
||||
}
|
||||
|
||||
int compare;
|
||||
int tagIndex = 0;
|
||||
int curIndex = start;
|
||||
|
||||
for (; ; ) {
|
||||
int tagByte = tag[tagIndex] & 0xff;
|
||||
int curByte = tags[curIndex] & 0xff;
|
||||
compare = tagByte - curByte;
|
||||
if (compare != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
tagIndex++;
|
||||
curIndex++;
|
||||
if (tagIndex == tag.length && curIndex == start + middle) {
|
||||
break;
|
||||
}
|
||||
if (tagIndex == tag.length) {
|
||||
compare = -1;
|
||||
break;
|
||||
}
|
||||
if (curIndex == start + middle) {
|
||||
compare = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compare < 0) {
|
||||
high = start - 1;
|
||||
} else if (compare > 0) {
|
||||
low = start + end + 1;
|
||||
} else {
|
||||
return new String(tags, start + middle + 1, end - middle - 1, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ArrayList<Pair<String, String>> suggest(String keyword) {
|
||||
return searchTag(tags, keyword);
|
||||
}
|
||||
|
||||
private ArrayList<Pair<String, String>> searchTag(byte[] tags, String keyword) {
|
||||
ArrayList<Pair<String, String>> searchHints = new ArrayList<>();
|
||||
int begin = 0;
|
||||
while (begin < tags.length - 1) {
|
||||
int start = begin;
|
||||
// Look for the starting '\n'
|
||||
while (tags[start] != '\n') {
|
||||
start++;
|
||||
}
|
||||
|
||||
// Look for the middle '\r'.
|
||||
int middle = 1;
|
||||
while (tags[start + middle] != '\r') {
|
||||
middle++;
|
||||
}
|
||||
|
||||
// Look for the ending '\n'
|
||||
int end = middle + 1;
|
||||
while (tags[start + end] != '\n') {
|
||||
end++;
|
||||
}
|
||||
|
||||
begin = start + end;
|
||||
|
||||
byte[] hintBytes = new byte[end - middle - 1];
|
||||
System.arraycopy(tags, start + middle + 1, hintBytes, 0, end - middle - 1);
|
||||
String hint = new String(hintBytes, StandardCharsets.UTF_8);
|
||||
byte[] tagBytes = new byte[middle];
|
||||
System.arraycopy(tags, start + 1, tagBytes, 0, middle);
|
||||
String tag = new String(tagBytes, StandardCharsets.UTF_8);
|
||||
int index = tag.indexOf(':');
|
||||
boolean keywordMatches;
|
||||
if (index == -1 || index >= tag.length() - 1 || keyword.length() > 2) {
|
||||
keywordMatches = containsIgnoreSpace(tag, keyword);
|
||||
} else {
|
||||
keywordMatches = containsIgnoreSpace(tag.substring(index + 1), keyword);
|
||||
}
|
||||
|
||||
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
|
||||
Pair<String, String> pair = new Pair<>(hint, tag);
|
||||
if (!searchHints.contains(pair)) {
|
||||
searchHints.add(pair);
|
||||
}
|
||||
}
|
||||
if (searchHints.size() > 20) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return searchHints;
|
||||
}
|
||||
|
||||
private boolean containsIgnoreSpace(String text, String key) {
|
||||
return text.replace(" ", "").contains(key.replace(" ", ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.content.Context
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.HashCodeUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
object EhTagDatabase {
|
||||
private lateinit var tags: JSONObject
|
||||
private lateinit var tagList: ArrayList<Pair<String, String>>
|
||||
|
||||
fun isInitialized(): Boolean {
|
||||
return this::tags.isInitialized && this::tagList.isInitialized
|
||||
}
|
||||
|
||||
private fun updateData(source: BufferedSource) {
|
||||
try {
|
||||
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
|
||||
val tmpTagList = arrayListOf<Pair<String, String>>()
|
||||
tmpTags.keys().forEachRemaining { prefix: String ->
|
||||
val values = tmpTags.optJSONObject(prefix)
|
||||
values?.let {
|
||||
it.keys().forEachRemaining { tag: String ->
|
||||
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
tags = tmpTags
|
||||
tagList = tmpTagList
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTranslation(prefix: String?, tag: String?): String? {
|
||||
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/* Construct a cold flow for tag database suggestions */
|
||||
fun suggestFlow(
|
||||
keyword: String,
|
||||
translate: Boolean,
|
||||
exactly: Boolean = false
|
||||
): Flow<Pair<String?, String>> = flow {
|
||||
tagList.forEach {
|
||||
val tag = it.first
|
||||
val hint = if (translate) it.second else null
|
||||
val index = tag.indexOf(':')
|
||||
if (tag.substring(index + 1).trim() == keyword) {
|
||||
if (exactly)
|
||||
emit(Pair(hint, tag))
|
||||
return@forEach
|
||||
}
|
||||
if (exactly)
|
||||
return@forEach
|
||||
val keywordMatches: Boolean =
|
||||
if (index == -1 || index >= tag.length - 1 || keyword.length > 2) {
|
||||
containsIgnoreSpace(tag, keyword)
|
||||
} else {
|
||||
containsIgnoreSpace(tag.substring(index + 1), keyword)
|
||||
}
|
||||
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
|
||||
emit(Pair(hint, tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsIgnoreSpace(text: String?, key: String): Boolean {
|
||||
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
|
||||
}
|
||||
|
||||
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||
it["artist"] = "a"
|
||||
it["cosplayer"] = "cos"
|
||||
it["character"] = "c"
|
||||
it["female"] = "f"
|
||||
it["group"] = "g"
|
||||
it["language"] = "l"
|
||||
it["male"] = "m"
|
||||
it["mixed"] = "x"
|
||||
it["other"] = "o"
|
||||
it["parody"] = "p"
|
||||
it["reclass"] = "r"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun namespaceToPrefix(namespace: String): String? {
|
||||
return NAMESPACE_TO_PREFIX[namespace]
|
||||
}
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? {
|
||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
}
|
||||
|
||||
fun isTranslatable(context: Context): Boolean {
|
||||
return context.resources.getBoolean(R.bool.tag_translatable)
|
||||
}
|
||||
|
||||
private fun getFileContent(file: File): String? {
|
||||
try {
|
||||
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileSha1(file: File): String? {
|
||||
try {
|
||||
FileInputStream(file).use { stream ->
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
var n: Int
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
while (stream.read(buffer).also { n = it } != -1) {
|
||||
digest.update(buffer, 0, n)
|
||||
}
|
||||
return HashCodeUtils.bytesToHexString(digest.digest())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkData(sha1File: File, dataFile: File): Boolean {
|
||||
val s1 = getFileContent(sha1File) ?: return false
|
||||
val s2 = getFileSha1(dataFile) ?: return false
|
||||
return s1 == s2
|
||||
}
|
||||
|
||||
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
||||
val request: Request = Request.Builder().url(url).build()
|
||||
val call = client.newCall(request)
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return false
|
||||
}
|
||||
val body = response.body
|
||||
body.byteStream()
|
||||
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
|
||||
return true
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
FileUtils.delete(file)
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@JvmStatic
|
||||
fun update() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
updateInternal()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateInternal() {
|
||||
val urls = getMetadata(EhApplication.application)
|
||||
urls?.let {
|
||||
val sha1Name = urls[0]
|
||||
val sha1Url = urls[1]
|
||||
val dataName = urls[2]
|
||||
val dataUrl = urls[3]
|
||||
|
||||
try {
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
checkNotNull(dir)
|
||||
|
||||
// Check current sha1 and current data
|
||||
val sha1File = File(dir, sha1Name)
|
||||
val dataFile = File(dir, dataName)
|
||||
if (!checkData(sha1File, dataFile)) {
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
}
|
||||
|
||||
// Read current EhTagDatabase
|
||||
if (!isInitialized() && dataFile.exists()) {
|
||||
try {
|
||||
dataFile.source().buffer().use { updateData(it) }
|
||||
} catch (e: IOException) {
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
}
|
||||
}
|
||||
val client = okHttpClient
|
||||
|
||||
// Save new sha1
|
||||
val tempSha1File = File(dir, "$sha1Name.tmp")
|
||||
check(save(client, sha1Url, tempSha1File))
|
||||
|
||||
// Check new sha1 and current data
|
||||
if (checkData(tempSha1File, dataFile)) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File)
|
||||
return
|
||||
}
|
||||
|
||||
// Save new data
|
||||
val tempDataFile = File(dir, "$dataName.tmp")
|
||||
check(save(client, dataUrl, tempDataFile))
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1File, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File)
|
||||
FileUtils.delete(tempDataFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File)
|
||||
FileUtils.delete(dataFile)
|
||||
tempSha1File.renameTo(sha1File)
|
||||
tempDataFile.renameTo(dataFile)
|
||||
|
||||
// Read new EhTagDatabase
|
||||
try {
|
||||
dataFile.source().buffer().use { updateData(it) }
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
class EhX509TrustManager : X509TrustManager {
|
||||
object EhX509TrustManager : X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class FavListUrlBuilder implements Parcelable {
|
||||
|
||||
public static final int FAV_CAT_ALL = -1;
|
||||
public static final int FAV_CAT_LOCAL = -2;
|
||||
public static final Parcelable.Creator<FavListUrlBuilder> CREATOR = new Parcelable.Creator<FavListUrlBuilder>() {
|
||||
public static final Parcelable.Creator<FavListUrlBuilder> CREATOR = new Parcelable.Creator<>() {
|
||||
|
||||
@Override
|
||||
public FavListUrlBuilder createFromParcel(Parcel source) {
|
||||
@@ -66,12 +66,14 @@ public class FavListUrlBuilder implements Parcelable {
|
||||
return favCat >= 0 && favCat <= 9;
|
||||
}
|
||||
|
||||
public void setPrev(String prev) {
|
||||
mPrev = prev;
|
||||
}
|
||||
|
||||
public void setNext(String next) {
|
||||
mNext = next;
|
||||
public void setIndex(String index, boolean isNext) {
|
||||
if (isNext) {
|
||||
mNext = index;
|
||||
mPrev = null;
|
||||
} else {
|
||||
mPrev = index;
|
||||
mNext = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setJumpTo(String jumpTo) {
|
||||
@@ -115,12 +117,6 @@ public class FavListUrlBuilder implements Parcelable {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// Name
|
||||
ub.addQuery("sn", "on");
|
||||
// Tags
|
||||
ub.addQuery("st", "on");
|
||||
// Note
|
||||
ub.addQuery("sf", "on");
|
||||
}
|
||||
if (mPrev != null && !mPrev.isEmpty()) {
|
||||
ub.addQuery("prev", mPrev);
|
||||
|
||||
@@ -36,8 +36,8 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
@get:Mode
|
||||
@Mode
|
||||
var mode = MODE_NORMAL
|
||||
private var mPrevGid = 0
|
||||
private var mNextGid = 0
|
||||
private var mPrev: String? = null
|
||||
private var mNext: String? = null
|
||||
private var mJumpTo: String? = null
|
||||
var category = EhUtils.NONE
|
||||
private var mKeyword: String? = null
|
||||
@@ -54,8 +54,8 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
constructor()
|
||||
protected constructor(parcel: Parcel) {
|
||||
mode = parcel.readInt()
|
||||
mPrevGid = parcel.readInt()
|
||||
mNextGid = parcel.readInt()
|
||||
mPrev = parcel.readString()
|
||||
mNext = parcel.readString()
|
||||
mJumpTo = parcel.readString()
|
||||
this.category = parcel.readInt()
|
||||
mKeyword = parcel.readString()
|
||||
@@ -72,8 +72,8 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
|
||||
fun reset() {
|
||||
mode = MODE_NORMAL
|
||||
mPrevGid = 0
|
||||
mNextGid = 0
|
||||
mPrev = null
|
||||
mNext = null
|
||||
mJumpTo = null
|
||||
this.category = EhUtils.NONE
|
||||
mKeyword = null
|
||||
@@ -96,12 +96,14 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
fun setPrevGid(prevGid: Int) {
|
||||
mPrevGid = prevGid
|
||||
}
|
||||
|
||||
fun setNextGid(nextGid: Int) {
|
||||
mNextGid = nextGid
|
||||
fun setIndex(index: String?, isNext: Boolean = true) {
|
||||
if (isNext) {
|
||||
mNext = index
|
||||
mPrev = null
|
||||
} else {
|
||||
mPrev = index
|
||||
mNext = null
|
||||
}
|
||||
}
|
||||
|
||||
fun setJumpTo(jumpTo: String?) {
|
||||
@@ -121,8 +123,8 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
*/
|
||||
fun set(lub: ListUrlBuilder) {
|
||||
mode = lub.mode
|
||||
mPrevGid = lub.mPrevGid
|
||||
mNextGid = lub.mNextGid
|
||||
mPrev = lub.mPrev
|
||||
mNext = lub.mNext
|
||||
mJumpTo = lub.mJumpTo
|
||||
this.category = lub.category
|
||||
mKeyword = lub.mKeyword
|
||||
@@ -196,7 +198,7 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
return
|
||||
}
|
||||
val querys = StringUtils.split(query, '&')
|
||||
val queries = StringUtils.split(query, '&')
|
||||
var category = 0
|
||||
var keyword: String? = null
|
||||
var enableAdvanceSearch = false
|
||||
@@ -206,7 +208,7 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
var enablePage = false
|
||||
var pageFrom = -1
|
||||
var pageTo = -1
|
||||
for (str in querys) {
|
||||
for (str in queries) {
|
||||
val index = str.indexOf('=')
|
||||
if (index < 0) {
|
||||
continue
|
||||
@@ -348,19 +350,17 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSHash != null) {
|
||||
ub.addQuery("f_shash", mSHash!!)
|
||||
mSHash?.let {
|
||||
ub.addQuery("f_shash", it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
ub.addQuery("seek", mJumpTo!!)
|
||||
mPrevGid = 0
|
||||
mNextGid = 0
|
||||
ub.addQuery("seek", it)
|
||||
}
|
||||
if (mPrevGid != 0) {
|
||||
ub.addQuery("prev", mPrevGid)
|
||||
mPrev?.let {
|
||||
ub.addQuery("prev", it)
|
||||
}
|
||||
if (mNextGid != 0) {
|
||||
ub.addQuery("next", mNextGid)
|
||||
mNext?.let {
|
||||
ub.addQuery("next", it)
|
||||
}
|
||||
// Advance search
|
||||
if (advanceSearch != -1) {
|
||||
@@ -393,14 +393,14 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
// Empty
|
||||
}
|
||||
if (mPrevGid != 0) {
|
||||
sb.append("?prev=").append(mPrevGid)
|
||||
mPrev?.let {
|
||||
sb.append("?prev=").append(it)
|
||||
}
|
||||
if (mNextGid != 0) {
|
||||
sb.append("?next=").append(mNextGid)
|
||||
mNext?.let {
|
||||
sb.append("?next=").append(it)
|
||||
}
|
||||
if (mJumpTo != null) {
|
||||
sb.append("&seek=").append(mJumpTo)
|
||||
mJumpTo?.let {
|
||||
sb.append("&seek=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
@@ -413,14 +413,14 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
// Empty
|
||||
}
|
||||
if (mPrevGid != 0) {
|
||||
sb.append("?prev=").append(mPrevGid)
|
||||
mPrev?.let {
|
||||
sb.append("?prev=").append(it)
|
||||
}
|
||||
if (mNextGid != 0) {
|
||||
sb.append("?next=").append(mNextGid)
|
||||
mNext?.let {
|
||||
sb.append("?next=").append(it)
|
||||
}
|
||||
if (mJumpTo != null) {
|
||||
sb.append("&seek=").append(mJumpTo)
|
||||
mJumpTo?.let {
|
||||
sb.append("&seek=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
@@ -433,61 +433,15 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
try {
|
||||
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
// Empty
|
||||
e.printStackTrace()
|
||||
}
|
||||
if (mNextGid != 0) {
|
||||
sb.append("&p=").append(mNextGid)
|
||||
mJumpTo?.let {
|
||||
sb.append("&p=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
else -> {
|
||||
val url: String = if (mode == MODE_NORMAL) {
|
||||
EhUrl.getHost()
|
||||
} else {
|
||||
EhUrl.getWatchedUrl()
|
||||
}
|
||||
val ub = UrlBuilder(url)
|
||||
if (this.category != EhUtils.NONE) {
|
||||
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
|
||||
}
|
||||
if (mKeyword != null) {
|
||||
val keyword = mKeyword!!.trim { it <= ' ' }
|
||||
if (keyword.isNotEmpty()) {
|
||||
try {
|
||||
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
|
||||
} catch (_: UnsupportedEncodingException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSHash != null) {
|
||||
ub.addQuery("f_shash", mSHash!!)
|
||||
}
|
||||
if (mPrevGid != 0) {
|
||||
ub.addQuery("prev", mPrevGid)
|
||||
}
|
||||
if (mNextGid != 0) {
|
||||
ub.addQuery("next", mNextGid)
|
||||
}
|
||||
if (advanceSearch != -1) {
|
||||
ub.addQuery("advsearch", "1")
|
||||
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
|
||||
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
|
||||
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
|
||||
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
|
||||
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
|
||||
if (minRating != -1) {
|
||||
ub.addQuery("f_sr", "on")
|
||||
ub.addQuery("f_srdd", minRating)
|
||||
}
|
||||
if (pageFrom != -1 || pageTo != -1) {
|
||||
ub.addQuery("f_sp", "on")
|
||||
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
|
||||
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
|
||||
}
|
||||
}
|
||||
ub.build()
|
||||
}
|
||||
else -> throw java.lang.IllegalStateException("Unexpected value: $mode")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,8 +451,8 @@ open class ListUrlBuilder : Cloneable, Parcelable {
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeInt(mode)
|
||||
dest.writeInt(mPrevGid)
|
||||
dest.writeInt(mNextGid)
|
||||
dest.writeString(mPrev)
|
||||
dest.writeString(mNext)
|
||||
dest.writeString(mJumpTo)
|
||||
dest.writeInt(this.category)
|
||||
dest.writeString(mKeyword)
|
||||
|
||||
@@ -31,22 +31,17 @@ import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FavoritesParser {
|
||||
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+(-\\d+)?)");
|
||||
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+(-\\d+)?)");
|
||||
|
||||
public static Result parse(String body) throws Exception {
|
||||
if (body.contains("This page requires you to log on.</p>")) {
|
||||
throw new EhException(GetText.getString(R.string.need_sign_in));
|
||||
}
|
||||
Document d = Jsoup.parse(body);
|
||||
|
||||
String[] catArray = new String[10];
|
||||
int[] countArray = new int[10];
|
||||
try {
|
||||
Document d = Jsoup.parse(body);
|
||||
Element ido = JsoupUtils.getElementByClass(d, "ido");
|
||||
//noinspection ConstantConditions
|
||||
Elements fps = ido.getElementsByClass("fp");
|
||||
@@ -64,24 +59,13 @@ public class FavoritesParser {
|
||||
throw new ParseException("Parse favorites error", body);
|
||||
}
|
||||
|
||||
Result re = new Result();
|
||||
try {
|
||||
Element prev = d.getElementById("uprev");
|
||||
Element next = d.getElementById("unext");
|
||||
assert prev != null;
|
||||
assert next != null;
|
||||
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
|
||||
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
|
||||
if (matcherPrev.find()) re.prevPage = matcherPrev.group(1);
|
||||
if (matcherNext.find()) re.nextPage = matcherNext.group(1);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
GalleryListParser.Result result = GalleryListParser.parse(body);
|
||||
|
||||
Result re = new Result();
|
||||
re.catArray = catArray;
|
||||
re.countArray = countArray;
|
||||
re.prev = result.prev;
|
||||
re.next = result.next;
|
||||
re.galleryInfoList = result.galleryInfoList;
|
||||
|
||||
return re;
|
||||
@@ -90,8 +74,8 @@ public class FavoritesParser {
|
||||
public static class Result {
|
||||
public String[] catArray; // Size 10
|
||||
public int[] countArray; // Size 10
|
||||
public String prevPage;
|
||||
public String nextPage;
|
||||
public String prev;
|
||||
public String next;
|
||||
public List<GalleryInfo> galleryInfoList;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.client.EhFilter;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.GalleryComment;
|
||||
import com.hippo.ehviewer.client.data.GalleryCommentList;
|
||||
@@ -473,6 +474,13 @@ public class GalleryDetailParser {
|
||||
comment.user = c3.child(0).text();
|
||||
// comment
|
||||
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
|
||||
// filter comment
|
||||
if (!comment.uploader) {
|
||||
EhFilter sEhFilter = EhFilter.getInstance();
|
||||
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// last edited
|
||||
Element c8 = JsoupUtils.getElementByClass(element, "c8");
|
||||
if (c8 != null) {
|
||||
|
||||
@@ -50,8 +50,9 @@ public class GalleryListParser {
|
||||
private static final Pattern PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px;width:(\\d+)px");
|
||||
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("background-color:rgba\\((\\d+),(\\d+),(\\d+),");
|
||||
private static final Pattern PATTERN_PAGES = Pattern.compile("(\\d+) page");
|
||||
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+)");
|
||||
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+)");
|
||||
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("p=(\\d+)");
|
||||
private static final Pattern PATTERN_PREV = Pattern.compile("prev=(\\d+(-\\d+)?)");
|
||||
private static final Pattern PATTERN_NEXT = Pattern.compile("next=(\\d+(-\\d+)?)");
|
||||
|
||||
private static final String[][] FAVORITE_SLOT_RGB = new String[][]{
|
||||
new String[]{"0", "0", "0"},
|
||||
@@ -256,11 +257,7 @@ public class GalleryListParser {
|
||||
if (div != null) {
|
||||
gi.disowned = div.attr("style").contains("opacity:0.5");
|
||||
Element a = div.children().first();
|
||||
if (a != null) {
|
||||
gi.uploader = a.text().trim();
|
||||
} else {
|
||||
gi.uploader = div.text().trim();
|
||||
}
|
||||
gi.uploader = a == null ? div.text().trim() : a.text().trim();
|
||||
}
|
||||
}
|
||||
if (children.size() > pagesIndex) {
|
||||
@@ -296,28 +293,44 @@ public class GalleryListParser {
|
||||
Element next = d.getElementById("unext");
|
||||
assert prev != null;
|
||||
assert next != null;
|
||||
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
|
||||
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
|
||||
Matcher matcherPrev = PATTERN_PREV.matcher(prev.attr("href"));
|
||||
Matcher matcherNext = PATTERN_NEXT.matcher(next.attr("href"));
|
||||
if (matcherPrev.find())
|
||||
result.prevGid = NumberUtils.parseIntSafely(matcherPrev.group(1), 0);
|
||||
result.prev = matcherPrev.group(1);
|
||||
if (matcherNext.find())
|
||||
result.nextGid = NumberUtils.parseIntSafely(matcherNext.group(1), 0);
|
||||
result.founds = Integer.MAX_VALUE;
|
||||
result.next = matcherNext.group(1);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
result.noWatchedTags = body.contains("<p>You do not have any watched tags");
|
||||
if (body.contains("No hits found</p>")) {
|
||||
result.founds = 0;
|
||||
//noinspection unchecked
|
||||
result.galleryInfoList = Collections.EMPTY_LIST;
|
||||
return result;
|
||||
} else if (d.getElementsByClass("ptt").isEmpty()) {
|
||||
result.founds = 1;
|
||||
} else {
|
||||
result.founds = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
try { // For toplists
|
||||
Element ptt = d.getElementsByClass("ptt").first();
|
||||
if (ptt != null) {
|
||||
Elements es = ptt.child(0).child(0).children();
|
||||
result.pages = Integer.parseInt(es.get(es.size() - 2).text().trim());
|
||||
|
||||
Element e = es.get(es.size() - 1);
|
||||
if (e != null) {
|
||||
e = e.children().first();
|
||||
if (e != null) {
|
||||
String href = e.attr("href");
|
||||
Matcher matcher = PATTERN_NEXT_PAGE.matcher(href);
|
||||
if (matcher.find()) {
|
||||
result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
Element itg = d.getElementsByClass("itg").first();
|
||||
Elements es;
|
||||
@@ -350,9 +363,10 @@ public class GalleryListParser {
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
public int founds;
|
||||
public int prevGid;
|
||||
public int nextGid;
|
||||
public int pages;
|
||||
public int nextPage;
|
||||
public String prev;
|
||||
public String next;
|
||||
public boolean noWatchedTags;
|
||||
public List<GalleryInfo> galleryInfoList;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package com.hippo.ehviewer.download;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -44,7 +43,6 @@ import com.hippo.yorozuya.collect.SparseJLArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -55,8 +53,7 @@ import java.util.Map;
|
||||
public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
|
||||
private static final String TAG = DownloadManager.class.getSimpleName();
|
||||
private static final Comparator<DownloadInfo> DATE_DESC_COMPARATOR = (lhs, rhs) -> lhs.time - rhs.time > 0 ? -1 : 1;
|
||||
private final Context mContext;
|
||||
private static final Comparator<DownloadInfo> DATE_DESC_COMPARATOR = (lhs, rhs) -> (int) (rhs.time - lhs.time);
|
||||
// All download info list
|
||||
private final LinkedList<DownloadInfo> mAllInfoList;
|
||||
// All download info map
|
||||
@@ -79,8 +76,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
@Nullable
|
||||
private SpiderQueen mCurrentSpider;
|
||||
|
||||
public DownloadManager(Context context) {
|
||||
mContext = context;
|
||||
public DownloadManager() {
|
||||
|
||||
// Get all labels
|
||||
List<DownloadLabel> labels = EhDB.getAllDownloadLabelList();
|
||||
@@ -206,7 +202,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
// Get download from wait list
|
||||
if (!mWaitList.isEmpty()) {
|
||||
DownloadInfo info = mWaitList.removeFirst();
|
||||
SpiderQueen spider = SpiderQueen.obtainSpiderQueen(mContext, info, SpiderQueen.MODE_DOWNLOAD);
|
||||
SpiderQueen spider = SpiderQueen.obtainSpiderQueen(info, SpiderQueen.MODE_DOWNLOAD);
|
||||
mCurrentTask = info;
|
||||
mCurrentSpider = spider;
|
||||
spider.addOnSpiderListener(this);
|
||||
@@ -387,7 +383,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
list.add(info);
|
||||
// Sort
|
||||
Collections.sort(list, DATE_DESC_COMPARATOR);
|
||||
list.sort(DATE_DESC_COMPARATOR);
|
||||
|
||||
// Add to all download list and map
|
||||
mAllInfoList.add(info);
|
||||
@@ -398,7 +394,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
|
||||
// Sort all download list
|
||||
Collections.sort(mAllInfoList, DATE_DESC_COMPARATOR);
|
||||
mAllInfoList.sort(DATE_DESC_COMPARATOR);
|
||||
|
||||
// Notify
|
||||
if (notify) {
|
||||
@@ -730,7 +726,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
srcList.remove(info);
|
||||
dstList.add(info);
|
||||
info.label = label;
|
||||
Collections.sort(dstList, DATE_DESC_COMPARATOR);
|
||||
dstList.sort(DATE_DESC_COMPARATOR);
|
||||
|
||||
// Save to DB
|
||||
EhDB.putDownloadInfo(info);
|
||||
@@ -830,7 +826,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
|
||||
// Sort
|
||||
Collections.sort(mDefaultInfoList, DATE_DESC_COMPARATOR);
|
||||
mDefaultInfoList.sort(DATE_DESC_COMPARATOR);
|
||||
|
||||
// Notify listener
|
||||
for (DownloadInfoListener l : mDownloadInfoListeners) {
|
||||
@@ -1052,7 +1048,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
@Override
|
||||
public void run() {
|
||||
switch (mType) {
|
||||
case TYPE_ON_GET_PAGES: {
|
||||
case TYPE_ON_GET_PAGES -> {
|
||||
DownloadInfo info = mCurrentTask;
|
||||
if (info == null) {
|
||||
Log.e(TAG, "Current task is null, but it should not be");
|
||||
@@ -1065,19 +1061,15 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_ON_GET_509: {
|
||||
case TYPE_ON_GET_509 -> {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener.onGet509();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_ON_PAGE_DOWNLOAD: {
|
||||
mSpeedReminder.onDownload(mIndex, mContentLength, mReceivedSize, mBytesRead);
|
||||
break;
|
||||
}
|
||||
case TYPE_ON_PAGE_SUCCESS: {
|
||||
case TYPE_ON_PAGE_DOWNLOAD ->
|
||||
mSpeedReminder.onDownload(mIndex, mContentLength, mReceivedSize, mBytesRead);
|
||||
case TYPE_ON_PAGE_SUCCESS -> {
|
||||
mSpeedReminder.onDone(mIndex);
|
||||
DownloadInfo info = mCurrentTask;
|
||||
if (info == null) {
|
||||
@@ -1096,9 +1088,8 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_ON_PAGE_FAILURE: {
|
||||
case TYPE_ON_PAGE_FAILURE -> {
|
||||
mSpeedReminder.onDone(mIndex);
|
||||
DownloadInfo info = mCurrentTask;
|
||||
if (info == null) {
|
||||
@@ -1114,9 +1105,8 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_ON_FINISH: {
|
||||
case TYPE_ON_FINISH -> {
|
||||
mSpeedReminder.onFinish();
|
||||
// Download done
|
||||
DownloadInfo info = mCurrentTask;
|
||||
@@ -1159,7 +1149,6 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
|
||||
}
|
||||
// Start next download
|
||||
ensureDownload();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (archiveThread != null) {
|
||||
archiveThread.interrupt();
|
||||
archiveThread = null;
|
||||
@@ -334,12 +335,9 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
Image image = null;
|
||||
if (buffer != null) {
|
||||
try {
|
||||
image = Image.decode(buffer, () -> {
|
||||
archiveAccessor.releaseByteBuffer(buffer);
|
||||
return null;
|
||||
});
|
||||
image = Image.decode(buffer.asReadOnlyBuffer());
|
||||
} catch (ImageDecoder.DecodeException e) {
|
||||
archiveAccessor.releaseByteBuffer(buffer);
|
||||
UriArchiveAccessor.releaseByteBuffer(buffer);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.FilenameFilter;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.NaturalComparator;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.StringUtils;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
public class DirPageLoader extends PageLoader2 implements Runnable {
|
||||
|
||||
private static final String TAG = DirPageLoader.class.getSimpleName();
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
private static final FilenameFilter imageFilter =
|
||||
(dir, name) -> StringUtils.endsWith(name.toLowerCase(), SUPPORT_IMAGE_EXTENSIONS);
|
||||
private static final Comparator<UniFile> naturalComparator = new Comparator<UniFile>() {
|
||||
private final NaturalComparator comparator = new NaturalComparator();
|
||||
|
||||
@Override
|
||||
public int compare(UniFile o1, UniFile o2) {
|
||||
return comparator.compare(o1.getName(), o2.getName());
|
||||
}
|
||||
};
|
||||
private final UniFile mDir;
|
||||
private final Stack<Integer> mRequests = new Stack<>();
|
||||
private final AtomicInteger mDecodingIndex = new AtomicInteger(-1);
|
||||
private final AtomicReference<UniFile[]> mFileList = new AtomicReference<>();
|
||||
@Nullable
|
||||
private Thread mBgThread;
|
||||
private volatile int mSize = PageLoader.STATE_WAIT;
|
||||
private String mError;
|
||||
|
||||
public DirPageLoader(@NonNull UniFile dir) {
|
||||
mDir = dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mBgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(),
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
mBgThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (mBgThread != null) {
|
||||
mBgThread.interrupt();
|
||||
mBgThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
synchronized (mRequests) {
|
||||
if (!mRequests.contains(index) && index != mDecodingIndex.get()) {
|
||||
mRequests.add(index);
|
||||
mRequests.notify();
|
||||
}
|
||||
}
|
||||
notifyPageWait(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceRequest(int index) {
|
||||
onRequest(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelRequest(int index) {
|
||||
synchronized (mRequests) {
|
||||
mRequests.remove(Integer.valueOf(index));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
return mError;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilename(int index) {
|
||||
UniFile[] fileList = mFileList.get();
|
||||
if (null == fileList || index < 0 || index >= fileList.length) {
|
||||
return Integer.toString(index);
|
||||
}
|
||||
UniFile src = fileList[index];
|
||||
String name = src.getName();
|
||||
if (name == null) {
|
||||
return Integer.toString(index);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilenameWithExtension(int index) {
|
||||
UniFile[] fileList = mFileList.get();
|
||||
if (null == fileList || index < 0 || index >= fileList.length) {
|
||||
return Integer.toString(index);
|
||||
}
|
||||
UniFile src = fileList[index];
|
||||
String extension = FileUtils.getExtensionFromFilename(src.getName());
|
||||
String name = src.getName();
|
||||
if (name == null) {
|
||||
return Integer.toString(index);
|
||||
}
|
||||
name = null != extension ? name + "." + extension : name;
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
UniFile[] fileList = mFileList.get();
|
||||
if (null == fileList || index < 0 || index >= fileList.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = fileList[index].openInputStream();
|
||||
os = file.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
UniFile[] fileList = mFileList.get();
|
||||
if (null == fileList || index < 0 || index >= fileList.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UniFile src = fileList[index];
|
||||
String extension = FileUtils.getExtensionFromFilename(src.getName());
|
||||
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
|
||||
if (null == dst) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = src.openInputStream();
|
||||
os = dst.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return dst;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// It may take a long time, so run it in new thread
|
||||
UniFile[] files = mDir.listFiles(imageFilter);
|
||||
|
||||
if (files == null) {
|
||||
mSize = PageLoader.STATE_ERROR;
|
||||
mError = GetText.getString(R.string.error_not_folder_path);
|
||||
|
||||
// Notify to to show error
|
||||
notifyDataChanged();
|
||||
|
||||
Log.i(TAG, "ImageDecoder end with error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort it
|
||||
Arrays.sort(files, naturalComparator);
|
||||
|
||||
// Put file list
|
||||
mFileList.lazySet(files);
|
||||
|
||||
// Set state normal and notify
|
||||
mSize = files.length;
|
||||
notifyDataChanged();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
synchronized (mRequests) {
|
||||
if (mRequests.isEmpty()) {
|
||||
try {
|
||||
mRequests.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Interrupted
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
index = mRequests.pop();
|
||||
mDecodingIndex.lazySet(index);
|
||||
}
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= files.length) {
|
||||
mDecodingIndex.lazySet(-1);
|
||||
notifyPageFailed(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = files[index].openInputStream();
|
||||
Image image = Image.decode((FileInputStream) is);
|
||||
mDecodingIndex.lazySet(-1);
|
||||
notifyPageSucceed(index, image);
|
||||
} catch (IOException e) {
|
||||
mDecodingIndex.lazySet(-1);
|
||||
notifyPageFailed(index);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
}
|
||||
mDecodingIndex.lazySet(-1);
|
||||
}
|
||||
|
||||
// Clear file list
|
||||
mFileList.lazySet(null);
|
||||
|
||||
Log.i(TAG, "ImageDecoder end");
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -33,19 +31,17 @@ import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
|
||||
|
||||
private final Context mContext;
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
@Nullable
|
||||
private SpiderQueen mSpiderQueen;
|
||||
|
||||
public EhPageLoader(Context context, GalleryInfo galleryInfo) {
|
||||
mContext = context;
|
||||
public EhPageLoader(GalleryInfo galleryInfo) {
|
||||
mGalleryInfo = galleryInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mContext, mGalleryInfo, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen.addOnSpiderListener(this);
|
||||
if (mSpiderQueen.size() > 0)
|
||||
notifyDataChanged();
|
||||
@@ -53,6 +49,7 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.removeOnSpiderListener(this);
|
||||
// Activity recreate may called, so wait 3000s
|
||||
@@ -126,7 +123,7 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
Object object = mSpiderQueen.request(index);
|
||||
Object object = mSpiderQueen.request(index, true);
|
||||
if (object instanceof Float) {
|
||||
notifyPagePercent(index, (Float) object);
|
||||
} else if (object instanceof String) {
|
||||
@@ -186,7 +183,9 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
|
||||
@Override
|
||||
public void onPageSuccess(int index, int finished, int downloaded, int total) {
|
||||
notifyDataChanged(index);
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.request(index, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -141,14 +141,11 @@ public final class SpiderDen {
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
switch (mMode) {
|
||||
case SpiderQueen.MODE_READ:
|
||||
return sCache != null;
|
||||
case SpiderQueen.MODE_DOWNLOAD:
|
||||
return mDownloadDir != null && mDownloadDir.isDirectory();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return switch (mMode) {
|
||||
case SpiderQueen.MODE_READ -> sCache != null;
|
||||
case SpiderQueen.MODE_DOWNLOAD -> mDownloadDir != null && mDownloadDir.isDirectory();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.hippo.ehviewer.spider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.os.AsyncTask;
|
||||
@@ -31,7 +30,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -66,6 +64,7 @@ import com.hippo.yorozuya.thread.PriorityThreadFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -83,6 +82,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import coil.disk.DiskCache;
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
@@ -113,7 +113,7 @@ public final class SpiderQueen implements Runnable {
|
||||
@NonNull
|
||||
private final OkHttpClient mHttpClient;
|
||||
@NonNull
|
||||
private final SimpleDiskCache mSpiderInfoCache;
|
||||
private final DiskCache mSpiderInfoCache;
|
||||
@NonNull
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
@NonNull
|
||||
@@ -176,13 +176,11 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public static SpiderQueen obtainSpiderQueen(@NonNull Context context,
|
||||
@NonNull GalleryInfo galleryInfo, @Mode int mode) {
|
||||
public static SpiderQueen obtainSpiderQueen(@NonNull GalleryInfo galleryInfo, @Mode int mode) {
|
||||
OSUtils.checkMainLoop();
|
||||
|
||||
SpiderQueen queen = sQueenMap.get(galleryInfo.gid);
|
||||
if (queen == null) {
|
||||
context.getApplicationContext();
|
||||
queen = new SpiderQueen(galleryInfo);
|
||||
sQueenMap.put(galleryInfo.gid, queen);
|
||||
// Set mode
|
||||
@@ -418,8 +416,8 @@ public final class SpiderQueen implements Runnable {
|
||||
return request(index, true, true, false);
|
||||
}
|
||||
|
||||
public Object request(int index) {
|
||||
return request(index, true, false, true);
|
||||
public Object request(int index, boolean addNeighbor) {
|
||||
return request(index, true, false, addNeighbor);
|
||||
}
|
||||
|
||||
private int getPageState(int index) {
|
||||
@@ -527,7 +525,7 @@ public final class SpiderQueen implements Runnable {
|
||||
synchronized (mDecodeRequestQueue) {
|
||||
if (!contain(mDecodeIndexArray, index) && !mDecodeRequestQueue.contains(index)) {
|
||||
mDecodeRequestQueue.add(index);
|
||||
mDecodeRequestQueue.notify();
|
||||
mDecodeRequestQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
result = null;
|
||||
@@ -692,21 +690,16 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
// Read from cache
|
||||
InputStreamPipe pipe = mSpiderInfoCache.getInputStreamPipe(Long.toString(mGalleryInfo.gid));
|
||||
if (null != pipe) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
spiderInfo = SpiderInfo.read(pipe.open());
|
||||
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid &&
|
||||
spiderInfo.token.equals(mGalleryInfo.token)) {
|
||||
return spiderInfo;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
var entry = mSpiderInfoCache.get(Long.toString(mGalleryInfo.gid));
|
||||
if (entry == null) return null;
|
||||
try (entry; InputStream is = new FileInputStream(entry.getData().toFile())) {
|
||||
spiderInfo = SpiderInfo.read(is);
|
||||
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid &&
|
||||
spiderInfo.token.equals(mGalleryInfo.token)) {
|
||||
return spiderInfo;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -846,16 +839,14 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
// Read from cache
|
||||
OutputStreamPipe pipe = mSpiderInfoCache.getOutputStreamPipe(Long.toString(mGalleryInfo.gid));
|
||||
try {
|
||||
pipe.obtain();
|
||||
spiderInfo.write(pipe.open());
|
||||
// Write to cache
|
||||
var editor = mSpiderInfoCache.edit(Long.toString(mGalleryInfo.gid));
|
||||
if (editor == null) return;
|
||||
try (OutputStream os = new FileOutputStream(editor.getData().toFile())) {
|
||||
spiderInfo.write(os);
|
||||
editor.commit();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
editor.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,12 +1081,7 @@ public final class SpiderQueen implements Runnable {
|
||||
|
||||
private String getPageUrl(long gid, int index, String pToken,
|
||||
String oldPageUrl, String skipHathKey) {
|
||||
String pageUrl;
|
||||
if (oldPageUrl != null) {
|
||||
pageUrl = oldPageUrl;
|
||||
} else {
|
||||
pageUrl = EhUrl.getPageUrl(gid, index, pToken);
|
||||
}
|
||||
String pageUrl = oldPageUrl != null ? oldPageUrl : EhUrl.getPageUrl(gid, index, pToken);
|
||||
// Add skipHathKey
|
||||
if (skipHathKey != null) {
|
||||
if (pageUrl.contains("?")) {
|
||||
@@ -1251,10 +1237,7 @@ public final class SpiderQueen implements Runnable {
|
||||
Log.d(TAG, "Start download image " + index);
|
||||
}
|
||||
|
||||
// disable Call Timeout for image-downloading requests
|
||||
Call call = mHttpClient.newBuilder()
|
||||
.callTimeout(0, TimeUnit.SECONDS).build()
|
||||
.newCall(new EhRequestBuilder(targetImageUrl, referer).build());
|
||||
Call call = mHttpClient.newCall(new EhRequestBuilder(targetImageUrl, referer).build());
|
||||
Response response = call.execute();
|
||||
ResponseBody responseBody = response.body();
|
||||
|
||||
|
||||
@@ -16,15 +16,11 @@
|
||||
package com.hippo.ehviewer.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.content.res.Resources.Theme
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StyleRes
|
||||
@@ -33,53 +29,30 @@ import androidx.core.content.ContextCompat
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import rikka.core.res.resolveColor
|
||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||
|
||||
abstract class EhActivity : AppCompatActivity() {
|
||||
@StyleRes
|
||||
fun getThemeStyleRes(context: Context): Int {
|
||||
return if (THEME_BLACK == getTheme(context)) {
|
||||
R.style.ThemeOverlay_Black
|
||||
} else R.style.ThemeOverlay
|
||||
fun getThemeStyleRes(): Int {
|
||||
val isBlackDarkTheme = Settings.getBoolean("black_dark_theme", false)
|
||||
return if (isBlackDarkTheme && isNightMode()) R.style.ThemeOverlay_Black else R.style.ThemeOverlay
|
||||
}
|
||||
|
||||
override fun onApplyThemeResource(theme: Theme, resid: Int, first: Boolean) {
|
||||
if (parent == null) {
|
||||
theme.applyStyle(resid, true)
|
||||
} else {
|
||||
try {
|
||||
theme.setTo(parent.theme)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
theme.applyStyle(resid, false)
|
||||
}
|
||||
theme.applyStyle(getThemeStyleRes(this), true)
|
||||
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first)
|
||||
theme.applyStyle(resid, true)
|
||||
theme.applyStyle(getThemeStyleRes(), true)
|
||||
}
|
||||
|
||||
override fun onNightModeChanged(mode: Int) {
|
||||
theme.applyStyle(getThemeStyleRes(this), true)
|
||||
theme.applyStyle(getThemeStyleRes(), true)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
layoutInflater.factory2 =
|
||||
LayoutInflaterFactory(delegate).addOnViewCreatedListener(
|
||||
WindowInsetsHelper.LISTENER
|
||||
)
|
||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate).addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||
super.onCreate(savedInstanceState)
|
||||
window.decorView.post {
|
||||
val rootWindowInsets = window.decorView.rootWindowInsets
|
||||
if (rootWindowInsets != null && rootWindowInsets.systemWindowInsetBottom >= Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
|
||||
window.navigationBarColor =
|
||||
theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
(application as EhApplication).registerActivity(this)
|
||||
}
|
||||
|
||||
@@ -89,56 +62,24 @@ abstract class EhActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val locked_resume_time = System.currentTimeMillis() / 1000
|
||||
val locked_delay_time = locked_resume_time - EhApplication.locked_last_leave_time
|
||||
if (locked_delay_time < Settings.getSecurityDelay() * 60) {
|
||||
val lockedResumeTime = System.currentTimeMillis() / 1000
|
||||
val lockedDelayTime = lockedResumeTime - EhApplication.locked_last_leave_time
|
||||
if (lockedDelayTime < Settings.getSecurityDelay() * 60) {
|
||||
EhApplication.locked = false
|
||||
} else if (Settings.getSecurity() && isAuthenticationSupported() && EhApplication.locked) {
|
||||
startActivity(Intent(this, SecurityActivity::class.java))
|
||||
}
|
||||
super.onResume()
|
||||
if (Settings.getEnabledSecurity()) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
window.setSecureScreen(Settings.getEnabledSecurity())
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) {
|
||||
Settings.putNotificationRequired()
|
||||
}
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { Settings.putNotificationRequired() }
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
fun checkAndRequestNotificationPermission() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (Settings.getNotificationRequired())
|
||||
return
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) return
|
||||
if (Settings.getNotificationRequired()) return
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val THEME_DEFAULT = "DEFAULT"
|
||||
private const val THEME_BLACK = "BLACK"
|
||||
private val isBlackNightTheme: Boolean
|
||||
get() = Settings.getBoolean("black_dark_theme", false)
|
||||
|
||||
fun getTheme(context: Context): String {
|
||||
return if (isBlackNightTheme && isNightMode(context.resources.configuration)) THEME_BLACK else THEME_DEFAULT
|
||||
}
|
||||
|
||||
private fun isNightMode(configuration: Configuration): Boolean {
|
||||
return configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.util.addTextToClipboard
|
||||
import com.hippo.util.tellClipboard
|
||||
|
||||
class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment() {
|
||||
private var mKeys: ArrayList<String> = arrayListOf()
|
||||
@@ -117,10 +118,7 @@ class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment(
|
||||
if (position == INDEX_PARENT) {
|
||||
UrlOpener.openUrl(context, mValues[position], true)
|
||||
} else {
|
||||
activity?.addTextToClipboard(
|
||||
mValues[position],
|
||||
false,
|
||||
)
|
||||
requireActivity() tellClipboard mValues[position]
|
||||
if (position == INDEX_URL) {
|
||||
// Save it to avoid detect the gallery
|
||||
Settings.putClipboardTextHashCode(mValues[position].hashCode())
|
||||
|
||||
@@ -28,11 +28,11 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -40,6 +40,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
@@ -49,7 +50,6 @@ import com.hippo.app.EditTextDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
import com.hippo.ehviewer.client.EhUrlOpener;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
@@ -287,6 +287,9 @@ public final class MainActivity extends StageActivity
|
||||
mDrawerLayout = (DrawerLayout) ViewUtils.$$(this, R.id.draw_view);
|
||||
mNavView = (NavigationView) ViewUtils.$$(this, R.id.nav_view);
|
||||
mRightDrawer = (DrawerView) ViewUtils.$$(this, R.id.right_drawer);
|
||||
var callback = new DrawerOnBackPressedCallback(false, mDrawerLayout);
|
||||
mDrawerLayout.addDrawerListener(callback);
|
||||
getOnBackPressedDispatcher().addCallback(callback);
|
||||
|
||||
if (mNavView != null) {
|
||||
mNavView.setNavigationItemSelectedListener(this);
|
||||
@@ -309,8 +312,6 @@ public final class MainActivity extends StageActivity
|
||||
} else {
|
||||
onRestore(savedInstanceState);
|
||||
}
|
||||
|
||||
EhTagDatabase.update(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -483,9 +484,9 @@ public final class MainActivity extends StageActivity
|
||||
baseScene.getLayoutInflater(), mRightDrawer, null);
|
||||
if (drawerView != null) {
|
||||
mRightDrawer.addView(drawerView);
|
||||
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
} else {
|
||||
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,6 +518,13 @@ public final class MainActivity extends StageActivity
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getDrawerLockMode(int edgeGravity) {
|
||||
if (mDrawerLayout != null) {
|
||||
return mDrawerLayout.getDrawerLockMode(edgeGravity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void openDrawer(int drawerGravity) {
|
||||
if (mDrawerLayout != null) {
|
||||
mDrawerLayout.openDrawer(drawerGravity);
|
||||
@@ -568,17 +576,6 @@ public final class MainActivity extends StageActivity
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (mDrawerLayout != null && (mDrawerLayout.isDrawerOpen(Gravity.LEFT) ||
|
||||
mDrawerLayout.isDrawerOpen(Gravity.RIGHT))) {
|
||||
mDrawerLayout.closeDrawers();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(MenuItem item) {
|
||||
// Don't select twice
|
||||
@@ -625,4 +622,40 @@ public final class MainActivity extends StageActivity
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static class DrawerOnBackPressedCallback extends OnBackPressedCallback implements DrawerLayout.DrawerListener {
|
||||
private final DrawerLayout mLayout;
|
||||
|
||||
public DrawerOnBackPressedCallback(boolean enabled, DrawerLayout drawerLayout) {
|
||||
super(enabled);
|
||||
mLayout = drawerLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
mLayout.closeDrawers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {}
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(@NonNull View drawerView) {
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(@NonNull View drawerView) {}
|
||||
|
||||
@Override
|
||||
public void onDrawerStateChanged(int newState) {
|
||||
switch (newState) {
|
||||
case DrawerLayout.STATE_DRAGGING, DrawerLayout.STATE_SETTLING -> setEnabled(true);
|
||||
case DrawerLayout.STATE_IDLE -> {
|
||||
if (!mLayout.isDrawerVisible(GravityCompat.START) && !mLayout.isDrawerVisible(GravityCompat.END))
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,6 @@ class SettingsActivity : EhActivity() {
|
||||
mFab = findViewById(R.id.fab)
|
||||
mAppbarLayout = findViewById(R.id.appbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) <= 0
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.hippo.ehviewer.ui.fragment;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -333,14 +332,14 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
Log.d("LocalFavorites", "now backup page " + status);
|
||||
EhDB.putLocalFavorites(result.galleryInfoList);
|
||||
|
||||
if (result.nextPage != null) {
|
||||
if (result.next != null) {
|
||||
try {
|
||||
Thread.sleep(Settings.getDownloadDelay());
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
favIndex++;
|
||||
favListUrlBuilder.setNext(result.nextPage);
|
||||
favListUrlBuilder.setIndex(result.next, true);
|
||||
request.setArgs(favListUrlBuilder.build());
|
||||
mClient.execute(request);
|
||||
} else {
|
||||
@@ -372,7 +371,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
String key = preference.getKey();
|
||||
if (KEY_APP_LANGUAGE.equals(key) && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
if ("system".equals(newValue)) {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
|
||||
} else {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags((String) newValue));
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
|
||||
|
||||
public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
setTitle(getFragmentTitle());
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getFragmentTitle() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
try {
|
||||
Fragment classObj = (Fragment) Class.forName(key).newInstance();
|
||||
requireActivity().getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.replace(R.id.fragment, classObj)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
} catch (ClassNotFoundException | IllegalAccessException |
|
||||
java.lang.InstantiationException ignored) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setTitle(@StringRes int string) {
|
||||
requireActivity().setTitle(string);
|
||||
}
|
||||
|
||||
public void showTip(@StringRes int id, int length) {
|
||||
((SettingsActivity) requireActivity()).showTip(getString(id), length);
|
||||
}
|
||||
|
||||
public void showTip(CharSequence message, int length) {
|
||||
((SettingsActivity) requireActivity()).showTip(message, length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.SettingsActivity
|
||||
|
||||
open class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setTitle(fragmentTitle)
|
||||
}
|
||||
|
||||
@get:StringRes
|
||||
open val fragmentTitle: Int
|
||||
get() = -1
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
val fragment = when (preference.key) {
|
||||
"eh" -> EhFragment()
|
||||
"download" -> DownloadFragment()
|
||||
"privacy" -> PrivacyFragment()
|
||||
"advance" -> AdvancedFragment()
|
||||
"about" -> AboutFragment()
|
||||
"filter" -> FilterFragment()
|
||||
"mytags" -> MyTagsFragment()
|
||||
"uconfig" -> UConfigFragment()
|
||||
"hosts" -> HostsFragment()
|
||||
else -> null
|
||||
}
|
||||
fragment?.let {
|
||||
requireActivity().supportFragmentManager
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.replace(R.id.fragment, it)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setTitle(@StringRes string: Int) {
|
||||
requireActivity().setTitle(string)
|
||||
}
|
||||
|
||||
fun showTip(@StringRes id: Int, length: Int) {
|
||||
(requireActivity() as SettingsActivity).showTip(getString(id), length)
|
||||
}
|
||||
|
||||
fun showTip(message: CharSequence?, length: Int) {
|
||||
(requireActivity() as SettingsActivity).showTip(message, length)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ package com.hippo.ehviewer.ui.fragment
|
||||
import android.app.Activity
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
@@ -36,6 +38,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
val listThumbSize = findPreference<Preference>(Settings.KEY_LIST_THUMB_SIZE)
|
||||
val detailSize = findPreference<Preference>(Settings.KEY_DETAIL_SIZE)
|
||||
val thumbSize = findPreference<Preference>(Settings.KEY_THUMB_SIZE)
|
||||
val thumbResolution = findPreference<Preference>(Settings.KEY_THUMB_RESOLUTION)
|
||||
val showTagTranslations = findPreference<Preference>(Settings.KEY_SHOW_TAG_TRANSLATIONS)
|
||||
val tagTranslationsSource = findPreference<Preference>("tag_translations_source")
|
||||
Settings.getDisplayName()?.let { account?.summary = it }
|
||||
@@ -47,7 +50,10 @@ class EhFragment : BasePreferenceFragment() {
|
||||
thumbSize!!.onPreferenceChangeListener = this
|
||||
showTagTranslations!!.onPreferenceChangeListener = this
|
||||
blackDarkTheme!!.onPreferenceChangeListener = this
|
||||
if (!EhTagDatabase.isPossible(requireActivity())) {
|
||||
thumbResolution!!.setSummaryProvider {
|
||||
getString(R.string.settings_eh_thumb_resolution_summary, (it as ListPreference).entry)
|
||||
}
|
||||
if (!EhTagDatabase.isTranslatable(requireActivity())) {
|
||||
preferenceScreen.removePreference(showTagTranslations)
|
||||
preferenceScreen.removePreference(tagTranslationsSource!!)
|
||||
}
|
||||
@@ -75,7 +81,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
} else if (Settings.KEY_SHOW_TAG_TRANSLATIONS == key) {
|
||||
if (java.lang.Boolean.TRUE == newValue) {
|
||||
EhTagDatabase.update(requireActivity())
|
||||
EhTagDatabase.update()
|
||||
}
|
||||
} else if (Settings.KEY_BLACK_DARK_THEME == key) {
|
||||
if (requireActivity().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0) {
|
||||
@@ -86,7 +92,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getFragmentTitle(): Int {
|
||||
return R.string.settings_eh
|
||||
}
|
||||
@get:StringRes
|
||||
override val fragmentTitle: Int
|
||||
get() = R.string.settings_eh
|
||||
}
|
||||
@@ -264,10 +264,14 @@ class FilterFragment : BaseFragment() {
|
||||
private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList
|
||||
private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList
|
||||
private val mTagNamespaceFilterList: List<Filter> = mEhFilter.tagNamespaceFilterList
|
||||
private val mCommenterFilterList: List<Filter> = mEhFilter.commenterFilterList
|
||||
private val mCommentFilterList: List<Filter> = mEhFilter.commentFilterList
|
||||
private var mTitleHeader: Filter? = null
|
||||
private var mUploaderHeader: Filter? = null
|
||||
private var mTagHeader: Filter? = null
|
||||
private var mTagNamespaceHeader: Filter? = null
|
||||
private var mCommenterHeader: Filter? = null
|
||||
private var mCommentHeader: Filter? = null
|
||||
fun size(): Int {
|
||||
var count = 0
|
||||
var size = mTitleFilterList.size
|
||||
@@ -278,6 +282,10 @@ class FilterFragment : BaseFragment() {
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mTagNamespaceFilterList.size
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mCommenterFilterList.size;
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mCommentFilterList.size;
|
||||
count += if (0 == size) 0 else size + 1
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -317,6 +325,24 @@ class FilterFragment : BaseFragment() {
|
||||
}
|
||||
return mTagNamespaceHeader!!
|
||||
}
|
||||
private val commenterHeader: Filter
|
||||
get() {
|
||||
if (null == mCommenterHeader) {
|
||||
mCommenterHeader = Filter()
|
||||
mCommenterHeader!!.mode = MODE_HEADER
|
||||
mCommenterHeader!!.text = getString(R.string.filter_commenter)
|
||||
}
|
||||
return mCommenterHeader!!
|
||||
}
|
||||
private val commentHeader: Filter
|
||||
get() {
|
||||
if (null == mCommentHeader) {
|
||||
mCommentHeader = Filter()
|
||||
mCommentHeader!!.mode = MODE_HEADER
|
||||
mCommentHeader!!.text = getString(R.string.filter_comment)
|
||||
}
|
||||
return mCommentHeader!!
|
||||
}
|
||||
|
||||
operator fun get(index: Int): Filter {
|
||||
var index1 = index
|
||||
@@ -356,6 +382,26 @@ class FilterFragment : BaseFragment() {
|
||||
return tagNamespaceHeader
|
||||
} else if (index1 <= size) {
|
||||
return mTagNamespaceFilterList[index1 - 1]
|
||||
} else {
|
||||
size + 1
|
||||
}
|
||||
}
|
||||
size = mCommenterFilterList.size
|
||||
if (0 != size) {
|
||||
index1 -= if (index1 == 0) {
|
||||
return commenterHeader
|
||||
} else if (index1 <= size) {
|
||||
return mCommenterFilterList[index1 - 1]
|
||||
} else {
|
||||
size + 1
|
||||
}
|
||||
}
|
||||
size = mCommentFilterList.size
|
||||
if (0 != size) {
|
||||
if (index1 == 0) {
|
||||
return commentHeader
|
||||
} else if (index1 <= size) {
|
||||
return mCommentFilterList[index1 - 1]
|
||||
}
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.SeekBarPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.hippo.ehviewer.R
|
||||
@@ -59,9 +60,9 @@ class PrivacyFragment : BasePreferenceFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFragmentTitle(): Int {
|
||||
return R.string.settings_privacy
|
||||
}
|
||||
@get:StringRes
|
||||
override val fragmentTitle: Int
|
||||
get() = R.string.settings_privacy
|
||||
|
||||
private fun setUnlockDelaySummary(value: Int) {
|
||||
unlockDelay.summary = if (value == 0) {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
public class ReadFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.read_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.settings_read;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
@@ -78,6 +79,14 @@ public abstract class BaseScene extends SceneFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getDrawerLockMode(int edgeGravity) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
return ((MainActivity) activity).getDrawerLockMode(edgeGravity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void openDrawer(int drawerGravity) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
@@ -190,9 +199,9 @@ public abstract class BaseScene extends SceneFragment {
|
||||
|
||||
// Update left drawer locked state
|
||||
if (needShowLeftDrawer()) {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
|
||||
} else {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
|
||||
}
|
||||
|
||||
// Update nav checked item
|
||||
@@ -200,11 +209,6 @@ public abstract class BaseScene extends SceneFragment {
|
||||
|
||||
// Hide soft ime
|
||||
hideSoftInput();
|
||||
|
||||
insetsController = getInsetsController();
|
||||
if (insetsController != null) {
|
||||
insetsController.setAppearanceLightStatusBars((requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) <= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
@@ -406,7 +407,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
|
||||
@Override
|
||||
public void onNavigationClick() {
|
||||
toggleDrawer(Gravity.LEFT);
|
||||
toggleDrawer(GravityCompat.START);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -444,10 +445,10 @@ public class DownloadsScene extends ToolbarScene
|
||||
}
|
||||
return true;
|
||||
} else if (id == R.id.action_open_download_labels) {
|
||||
openDrawer(Gravity.RIGHT);
|
||||
openDrawer(GravityCompat.END);
|
||||
return true;
|
||||
} else if (id == R.id.action_reset_reading_progress) {
|
||||
new BaseDialogBuilder(getContext())
|
||||
new BaseDialogBuilder(requireContext())
|
||||
.setMessage(R.string.reset_reading_progress_message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
@@ -566,14 +567,14 @@ public class DownloadsScene extends ToolbarScene
|
||||
@Override
|
||||
public void onStartDragHandler() {
|
||||
// Lock right drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndDragHandler() {
|
||||
// Restore right drawer
|
||||
if (null != mRecyclerView && !mRecyclerView.isInCustomChoice()) {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,11 +876,13 @@ public class DownloadsScene extends ToolbarScene
|
||||
private static class DownloadLabelHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView label;
|
||||
private final ImageView edit;
|
||||
private final ImageView option;
|
||||
|
||||
private DownloadLabelHolder(View itemView) {
|
||||
super(itemView);
|
||||
label = (TextView) ViewUtils.$$(itemView, R.id.tv_key);
|
||||
edit = (ImageView) ViewUtils.$$(itemView, R.id.iv_edit);
|
||||
option = (ImageView) ViewUtils.$$(itemView, R.id.iv_option);
|
||||
}
|
||||
}
|
||||
@@ -933,12 +936,12 @@ public class DownloadsScene extends ToolbarScene
|
||||
mLabel = label1;
|
||||
updateForLabel();
|
||||
updateView();
|
||||
closeDrawer(Gravity.RIGHT);
|
||||
closeDrawer(GravityCompat.END);
|
||||
}
|
||||
});
|
||||
if (position > 0) {
|
||||
holder.option.setVisibility(View.VISIBLE);
|
||||
holder.option.setOnClickListener(v -> {
|
||||
holder.edit.setVisibility(View.VISIBLE);
|
||||
holder.edit.setOnClickListener(v -> {
|
||||
if (context != null) {
|
||||
EditTextDialogBuilder builder = new EditTextDialogBuilder(
|
||||
context, label, getString(R.string.download_labels));
|
||||
@@ -1195,8 +1198,8 @@ public class DownloadsScene extends ToolbarScene
|
||||
mFabLayout.setExpanded(true);
|
||||
}
|
||||
// Lock drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1205,8 +1208,8 @@ public class DownloadsScene extends ToolbarScene
|
||||
mFabLayout.setExpanded(false);
|
||||
}
|
||||
// Unlock drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,7 +22,6 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.Gravity;
|
||||
@@ -34,6 +33,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
@@ -136,15 +136,13 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
@Override
|
||||
public void run() {
|
||||
if (mFabLayout != null) {
|
||||
mFabLayout.setSecondaryFabVisibilityAt(0, true);
|
||||
updateJumpFab(); // index: 0, 2
|
||||
mFabLayout.setSecondaryFabVisibilityAt(1, true);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(2, true);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(3, false);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(4, false);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(5, false);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(6, false);
|
||||
}
|
||||
updateJumpFab();
|
||||
}
|
||||
};
|
||||
private int mFavLocalCount = 0;
|
||||
@@ -346,7 +344,8 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
// Hide jump fab on local fav cat
|
||||
private void updateJumpFab() {
|
||||
if (mFabLayout != null && mUrlBuilder != null) {
|
||||
mFabLayout.setSecondaryFabVisibilityAt(1, mUrlBuilder.getFavCat() != FavListUrlBuilder.FAV_CAT_LOCAL);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(0, mUrlBuilder.getFavCat() != FavListUrlBuilder.FAV_CAT_LOCAL);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(2, mUrlBuilder.getFavCat() != FavListUrlBuilder.FAV_CAT_LOCAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,8 +426,6 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
mFabLayout.toggle();
|
||||
} else if (mSearchMode) {
|
||||
exitSearchMode();
|
||||
} else if (isSearchViewShown()) {
|
||||
hideSearchView();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
@@ -437,21 +434,21 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
@Override
|
||||
public void onStartDragHandler() {
|
||||
// Lock right drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndDragHandler() {
|
||||
// Restore right drawer
|
||||
if (null != mRecyclerView && !mRecyclerView.isInCustomChoice()) {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
}
|
||||
|
||||
showSearchBar();
|
||||
}
|
||||
|
||||
public boolean onItemClick(int position) {
|
||||
if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(Gravity.RIGHT)) {
|
||||
if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.END)) {
|
||||
// Skip if in search mode
|
||||
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
|
||||
return true;
|
||||
@@ -477,7 +474,7 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
updateJumpFab();
|
||||
mHelper.refresh();
|
||||
|
||||
closeDrawer(Gravity.RIGHT);
|
||||
closeDrawer(GravityCompat.END);
|
||||
|
||||
} else {
|
||||
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
|
||||
@@ -570,7 +567,7 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
.setSelection(toDate)
|
||||
.build();
|
||||
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
|
||||
datePicker.addOnPositiveButtonClickListener(v -> mHelper.goTo(v));
|
||||
datePicker.addOnPositiveButtonClickListener(v -> mHelper.goTo(v, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -582,17 +579,14 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
|
||||
if (!mRecyclerView.isInCustomChoice()) {
|
||||
switch (position) {
|
||||
case 0: // Open right
|
||||
openDrawer(Gravity.RIGHT);
|
||||
break;
|
||||
case 1: // Go to
|
||||
if (mHelper.canGoTo()) {
|
||||
showGoToDialog();
|
||||
}
|
||||
break;
|
||||
case 2: // Refresh
|
||||
mHelper.refresh();
|
||||
break;
|
||||
// Go to
|
||||
case 0 -> {
|
||||
if (mHelper.canGoTo()) showGoToDialog();
|
||||
}
|
||||
// Refresh
|
||||
case 1 -> mHelper.refresh();
|
||||
// Last page
|
||||
case 2 -> mHelper.goTo("1-0", false);
|
||||
}
|
||||
view.setExpanded(false);
|
||||
return;
|
||||
@@ -678,8 +672,8 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
mHelper.setRefreshLayoutEnable(false);
|
||||
}
|
||||
// Lock drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -693,8 +687,8 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
mHelper.setRefreshLayoutEnable(true);
|
||||
}
|
||||
// Unlock drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -738,28 +732,7 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
}
|
||||
|
||||
updateSearchBar();
|
||||
|
||||
int pages;
|
||||
if (result.nextPage == null)
|
||||
pages = mHelper.pgCounter + 1;
|
||||
else
|
||||
pages = Integer.MAX_VALUE;
|
||||
|
||||
String prev = result.prevPage, next = result.nextPage;
|
||||
switch (mHelper.operation) {
|
||||
case -1:
|
||||
if (prev != null) mHelper.upperPage = prev;
|
||||
break;
|
||||
case 1:
|
||||
if (next != null) mHelper.lowerPage = next;
|
||||
break;
|
||||
default:
|
||||
mHelper.upperPage = prev;
|
||||
mHelper.lowerPage = next;
|
||||
break;
|
||||
}
|
||||
|
||||
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
|
||||
mHelper.onGetPageData(taskId, 0, 0, result.prev, result.next, result.galleryInfoList);
|
||||
|
||||
if (mDrawerAdapter != null) {
|
||||
mDrawerAdapter.notifyDataSetChanged();
|
||||
@@ -784,9 +757,9 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
}
|
||||
|
||||
if (list.size() == 0) {
|
||||
mHelper.onGetPageData(taskId, 0, 0, Collections.EMPTY_LIST);
|
||||
mHelper.onGetPageData(taskId, 0, 0, null, null, Collections.EMPTY_LIST);
|
||||
} else {
|
||||
mHelper.onGetPageData(taskId, 1, 0, list);
|
||||
mHelper.onGetPageData(taskId, 1, 0, null, null, list);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(keyword)) {
|
||||
@@ -1077,18 +1050,13 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
}
|
||||
|
||||
private class FavoritesHelper extends GalleryInfoContentHelper {
|
||||
public String upperPage = null;
|
||||
public String lowerPage = null;
|
||||
public int operation = 0;
|
||||
public int pgCounter = 0;
|
||||
|
||||
@Override
|
||||
protected void getPageData(final int taskId, int type, int page) {
|
||||
protected void getPageData(final int taskId, int type, int page, String index, boolean isNext) {
|
||||
MainActivity activity = getMainActivity();
|
||||
if (null == activity || null == mUrlBuilder || null == mClient) {
|
||||
return;
|
||||
}
|
||||
pgCounter = page;
|
||||
|
||||
if (mEnableModify) {
|
||||
mEnableModify = false;
|
||||
@@ -1127,7 +1095,7 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
url = mUrlBuilder.build();
|
||||
}
|
||||
|
||||
mUrlBuilder.setNext(lowerPage);
|
||||
mUrlBuilder.setIndex(index, true);
|
||||
EhRequest request = new EhRequest();
|
||||
request.setMethod(EhClient.METHOD_MODIFY_FAVORITES);
|
||||
request.setCallback(new GetFavoritesListener(getContext(),
|
||||
@@ -1140,25 +1108,8 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
final String keyword = mUrlBuilder.getKeyword();
|
||||
SimpleHandler.getInstance().post(() -> onGetFavoritesLocal(keyword, taskId));
|
||||
} else {
|
||||
String prev = null, next = null;
|
||||
if (jumpTo != null) {
|
||||
next = Integer.toString(minGid);
|
||||
operation = 0;
|
||||
} else if (page != 0 && mHelper != null) {
|
||||
if (page >= mHelper.getPageForTop()) {
|
||||
next = lowerPage;
|
||||
operation = 1;
|
||||
} else {
|
||||
prev = upperPage;
|
||||
operation = -1;
|
||||
}
|
||||
}
|
||||
mUrlBuilder.setPrev(prev);
|
||||
mUrlBuilder.setNext(next);
|
||||
|
||||
mUrlBuilder.setIndex(index, isNext);
|
||||
mUrlBuilder.setJumpTo(jumpTo);
|
||||
jumpTo = null;
|
||||
|
||||
String url = mUrlBuilder.build();
|
||||
EhRequest request = new EhRequest();
|
||||
request.setMethod(EhClient.METHOD_GET_FAVORITES);
|
||||
@@ -1187,13 +1138,6 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeChanged(int positionStart, int itemCount) {
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyItemRangeChanged(positionStart, itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (mAdapter != null) {
|
||||
@@ -1217,36 +1161,5 @@ public class FavoritesScene extends SearchBarScene implements
|
||||
showSearchBar();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClearData() {
|
||||
super.onClearData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRefresh() {
|
||||
super.beforeRefresh();
|
||||
upperPage = null;
|
||||
lowerPage = null;
|
||||
operation = 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Parcelable saveInstanceState(@NonNull Parcelable superState) {
|
||||
Bundle bundle = (Bundle) super.saveInstanceState(superState);
|
||||
bundle.putString(KEY_UPPER_PAGE, upperPage);
|
||||
bundle.putString(KEY_LOWER_PAGE, lowerPage);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Parcelable restoreInstanceState(@NonNull Parcelable state) {
|
||||
Bundle bundle = (Bundle) state;
|
||||
upperPage = bundle.getString(KEY_UPPER_PAGE);
|
||||
lowerPage = bundle.getString(KEY_LOWER_PAGE);
|
||||
return super.restoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,7 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
|
||||
RecyclerView recyclerView = mRecyclerView;
|
||||
switch (type) {
|
||||
default:
|
||||
case GalleryAdapter.TYPE_LIST: {
|
||||
case GalleryAdapter.TYPE_LIST -> {
|
||||
int columnWidth = mResources.getDimensionPixelOffset(Settings.getDetailSizeResId());
|
||||
mLayoutManager.setColumnSize(columnWidth);
|
||||
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE);
|
||||
@@ -123,9 +122,8 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
recyclerView.addItemDecoration(mListDecoration);
|
||||
adjustPaddings();
|
||||
notifyDataSetChanged();
|
||||
break;
|
||||
}
|
||||
case GalleryAdapter.TYPE_GRID: {
|
||||
case GalleryAdapter.TYPE_GRID -> {
|
||||
int columnWidth = Settings.getThumbSize();
|
||||
mLayoutManager.setColumnSize(columnWidth);
|
||||
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE);
|
||||
@@ -141,7 +139,6 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
recyclerView.addItemDecoration(mGirdDecoration);
|
||||
adjustPaddings();
|
||||
notifyDataSetChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,16 +146,11 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
@NonNull
|
||||
@Override
|
||||
public GalleryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
int layoutId;
|
||||
switch (viewType) {
|
||||
default:
|
||||
case TYPE_LIST:
|
||||
layoutId = R.layout.item_gallery_list;
|
||||
break;
|
||||
case TYPE_GRID:
|
||||
layoutId = R.layout.item_gallery_grid;
|
||||
break;
|
||||
}
|
||||
int layoutId = switch (viewType) {
|
||||
case TYPE_LIST -> R.layout.item_gallery_list;
|
||||
case TYPE_GRID -> R.layout.item_gallery_grid;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + viewType);
|
||||
};
|
||||
|
||||
GalleryHolder holder = new GalleryHolder(mInflater.inflate(layoutId, parent, false));
|
||||
|
||||
@@ -169,6 +161,9 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
holder.thumb.setLayoutParams(lp);
|
||||
}
|
||||
|
||||
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, holder.getBindingAdapterPosition()));
|
||||
holder.card.setOnLongClickListener(v -> onItemLongClick(holder.itemView, holder.getBindingAdapterPosition()));
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
@@ -193,8 +188,7 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
}
|
||||
|
||||
switch (mType) {
|
||||
default:
|
||||
case TYPE_LIST: {
|
||||
case TYPE_LIST -> {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
|
||||
holder.title.setText(EhUtils.getSuitableTitle(gi));
|
||||
holder.uploader.setText(gi.uploader);
|
||||
@@ -223,9 +217,8 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
}
|
||||
holder.favourited.setVisibility((mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) ? View.VISIBLE : View.GONE);
|
||||
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.gid) ? View.VISIBLE : View.GONE);
|
||||
break;
|
||||
}
|
||||
case TYPE_GRID: {
|
||||
case TYPE_GRID -> {
|
||||
((TileThumb) holder.thumb).setThumbSize(gi.thumbWidth, gi.thumbHeight);
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
|
||||
View category = holder.category;
|
||||
@@ -238,15 +231,11 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
|
||||
((TriangleDrawable) drawable).setColor(color);
|
||||
}
|
||||
holder.simpleLanguage.setText(gi.simpleLanguage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update transition name
|
||||
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gi.gid));
|
||||
|
||||
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, position));
|
||||
holder.card.setOnLongClickListener(v -> onItemLongClick(holder.itemView, position));
|
||||
}
|
||||
|
||||
@IntDef({TYPE_LIST, TYPE_GRID})
|
||||
|
||||
@@ -62,6 +62,7 @@ import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.UrlOpener;
|
||||
import com.hippo.ehviewer.WindowInsetsAnimationHelper;
|
||||
import com.hippo.ehviewer.client.EhClient;
|
||||
import com.hippo.ehviewer.client.EhFilter;
|
||||
import com.hippo.ehviewer.client.EhRequest;
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.client.data.GalleryComment;
|
||||
@@ -69,6 +70,7 @@ import com.hippo.ehviewer.client.data.GalleryCommentList;
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentParser;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.scene.SceneFragment;
|
||||
import com.hippo.text.URLImageGetter;
|
||||
@@ -194,7 +196,6 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
mEditPanel,
|
||||
mFabLayout
|
||||
));
|
||||
mRefreshLayout.setColorSchemeColors(ResourcesKt.resolveColor(getTheme(), androidx.appcompat.R.attr.colorPrimary));
|
||||
mRefreshLayout.setOnRefreshListener(this);
|
||||
|
||||
Context context = requireContext();
|
||||
@@ -266,6 +267,45 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
onBackPressed();
|
||||
}
|
||||
|
||||
private void showFilterCommenterDialog(String commenter, int position) {
|
||||
Context context = getContext();
|
||||
if (context == null || commenter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
new BaseDialogBuilder(context)
|
||||
.setMessage(getString(R.string.filter_the_commenter, commenter))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Filter filter = new Filter();
|
||||
filter.mode = EhFilter.MODE_COMMENTER;
|
||||
filter.text = commenter;
|
||||
EhFilter.getInstance().addFilter(filter);
|
||||
hideComment(position);
|
||||
showTip(R.string.filter_added, LENGTH_SHORT);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void hideComment(int position) {
|
||||
if (mGalleryDetail == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GalleryComment[] oldCommentsList = mGalleryDetail.comments.comments;
|
||||
GalleryComment[] newCommentsList = new GalleryComment[oldCommentsList.length - 1];
|
||||
for (int i = 0, j = 0; i < oldCommentsList.length; i++) {
|
||||
if (i != position) {
|
||||
newCommentsList[j] = oldCommentsList[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
mGalleryDetail.comments.comments = newCommentsList;
|
||||
mAdapter.notifyDataSetChanged();
|
||||
updateView(true);
|
||||
}
|
||||
|
||||
private void voteComment(long id, int vote) {
|
||||
Context context = getContext();
|
||||
MainActivity activity = getMainActivity();
|
||||
@@ -340,6 +380,10 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
|
||||
menu.add(resources.getString(R.string.copy_comment_text));
|
||||
menuId.add(R.id.copy);
|
||||
if (!comment.uploader && !comment.editable) {
|
||||
menu.add(resources.getString(R.string.block_commenter));
|
||||
menuId.add(R.id.block_commenter);
|
||||
}
|
||||
if (comment.editable) {
|
||||
menu.add(resources.getString(R.string.edit_comment));
|
||||
menuId.add(R.id.edit_comment);
|
||||
@@ -365,6 +409,8 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
int id = menuId.get(which);
|
||||
if (id == R.id.copy) {
|
||||
ClipboardUtilKt.addTextToClipboard(requireActivity(), comment.comment, false);
|
||||
} else if (id == R.id.block_commenter) {
|
||||
showFilterCommenterDialog(comment.user, position);
|
||||
} else if (id == R.id.vote_up) {
|
||||
voteComment(comment.id, 1);
|
||||
} else if (id == R.id.vote_down) {
|
||||
|
||||
@@ -57,7 +57,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
@@ -261,7 +260,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
private Pair<String, String>[] mArchiveList;
|
||||
@State
|
||||
private int mState = STATE_INIT;
|
||||
private boolean mModifingFavorites;
|
||||
private boolean mModifyingFavorites;
|
||||
|
||||
@Nullable
|
||||
private static String getArtist(GalleryTagGroup[] tagGroups) {
|
||||
@@ -293,47 +292,21 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
|
||||
@StringRes
|
||||
private int getRatingText(float rating) {
|
||||
int resId;
|
||||
switch (Math.round(rating * 2)) {
|
||||
case 0:
|
||||
resId = R.string.rating0;
|
||||
break;
|
||||
case 1:
|
||||
resId = R.string.rating1;
|
||||
break;
|
||||
case 2:
|
||||
resId = R.string.rating2;
|
||||
break;
|
||||
case 3:
|
||||
resId = R.string.rating3;
|
||||
break;
|
||||
case 4:
|
||||
resId = R.string.rating4;
|
||||
break;
|
||||
case 5:
|
||||
resId = R.string.rating5;
|
||||
break;
|
||||
case 6:
|
||||
resId = R.string.rating6;
|
||||
break;
|
||||
case 7:
|
||||
resId = R.string.rating7;
|
||||
break;
|
||||
case 8:
|
||||
resId = R.string.rating8;
|
||||
break;
|
||||
case 9:
|
||||
resId = R.string.rating9;
|
||||
break;
|
||||
case 10:
|
||||
resId = R.string.rating10;
|
||||
break;
|
||||
default:
|
||||
resId = R.string.rating_none;
|
||||
break;
|
||||
}
|
||||
|
||||
return resId;
|
||||
return switch (Math.round(rating * 2)) {
|
||||
case 0 -> R.string.rating0;
|
||||
case 1 -> R.string.rating1;
|
||||
case 2 -> R.string.rating2;
|
||||
case 3 -> R.string.rating3;
|
||||
case 4 -> R.string.rating4;
|
||||
case 5 -> R.string.rating5;
|
||||
case 6 -> R.string.rating6;
|
||||
case 7 -> R.string.rating7;
|
||||
case 8 -> R.string.rating8;
|
||||
case 9 -> R.string.rating9;
|
||||
case 10 -> R.string.rating10;
|
||||
default -> R.string.rating_none;
|
||||
};
|
||||
}
|
||||
|
||||
private void handleArgs(Bundle args) {
|
||||
@@ -357,7 +330,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getGalleryDetailUrl(boolean allComment) {
|
||||
private String getGalleryDetailUrl() {
|
||||
long gid;
|
||||
String token;
|
||||
if (mGalleryDetail != null) {
|
||||
@@ -372,7 +345,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return EhUrl.getGalleryDetailUrl(gid, token, 0, allComment);
|
||||
return EhUrl.getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
// -1 for error
|
||||
@@ -388,18 +361,6 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
}
|
||||
}
|
||||
|
||||
private String getToken() {
|
||||
if (mGalleryDetail != null) {
|
||||
return mGalleryDetail.token;
|
||||
} else if (mGalleryInfo != null) {
|
||||
return mGalleryInfo.token;
|
||||
} else if (ACTION_GID_TOKEN.equals(mAction)) {
|
||||
return mToken;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getUploader() {
|
||||
if (mGalleryDetail != null) {
|
||||
return mGalleryDetail.uploader;
|
||||
@@ -452,7 +413,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
super.onResume();
|
||||
if (mRead != null) {
|
||||
try {
|
||||
PageLoader2 galleryProvider = new EhPageLoader(requireContext(), mGalleryInfo);
|
||||
PageLoader2 galleryProvider = new EhPageLoader(mGalleryInfo);
|
||||
galleryProvider.start();
|
||||
int startPage = galleryProvider.getStartPage();
|
||||
if (startPage != 0) {
|
||||
@@ -519,14 +480,14 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_open_in_other_app) {
|
||||
String url = getGalleryDetailUrl(false);
|
||||
String url = getGalleryDetailUrl();
|
||||
Activity activity = getMainActivity();
|
||||
if (null != url && null != activity) {
|
||||
UrlOpener.openUrl(activity, url, false);
|
||||
}
|
||||
} else if (itemId == R.id.action_refresh) {
|
||||
if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {
|
||||
adjustViewVisibility(STATE_REFRESH, true);
|
||||
adjustViewVisibility(STATE_REFRESH);
|
||||
request();
|
||||
}
|
||||
} else if (itemId == R.id.action_add_tag) {
|
||||
@@ -550,7 +511,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
@Override
|
||||
public View onCreateViewWithToolbar(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
@@ -655,16 +616,16 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
if (prepareData()) {
|
||||
if (mGalleryDetail != null) {
|
||||
bindViewSecond();
|
||||
adjustViewVisibility(STATE_NORMAL, false);
|
||||
adjustViewVisibility(STATE_NORMAL);
|
||||
} else if (mGalleryInfo != null) {
|
||||
bindViewFirst();
|
||||
adjustViewVisibility(STATE_REFRESH_HEADER, false);
|
||||
adjustViewVisibility(STATE_REFRESH_HEADER);
|
||||
} else {
|
||||
adjustViewVisibility(STATE_REFRESH, false);
|
||||
adjustViewVisibility(STATE_REFRESH);
|
||||
}
|
||||
} else {
|
||||
mTip.setText(R.string.error_cannot_find_gallery);
|
||||
adjustViewVisibility(STATE_FAILED, false);
|
||||
adjustViewVisibility(STATE_FAILED);
|
||||
}
|
||||
|
||||
EhApplication.getDownloadManager().addDownloadInfoListener(this);
|
||||
@@ -759,12 +720,12 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
private boolean request() {
|
||||
Context context = getContext();
|
||||
MainActivity activity = getMainActivity();
|
||||
String url = getGalleryDetailUrl(false);
|
||||
String url = getGalleryDetailUrl();
|
||||
if (null == context || null == activity || null == url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EhClient.Callback callback = new GetGalleryDetailListener(context,
|
||||
EhClient.Callback<?> callback = new GetGalleryDetailListener(context,
|
||||
activity.getStageId(), getTag());
|
||||
mRequestId = ((EhApplication) context.getApplicationContext()).putGlobalStuff(callback);
|
||||
EhRequest request = new EhRequest()
|
||||
@@ -776,7 +737,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
return true;
|
||||
}
|
||||
|
||||
private void adjustViewVisibility(int state, boolean animation) {
|
||||
private void adjustViewVisibility(int state) {
|
||||
if (state == mState) {
|
||||
return;
|
||||
}
|
||||
@@ -784,34 +745,29 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
return;
|
||||
}
|
||||
|
||||
int oldState = mState;
|
||||
mState = state;
|
||||
|
||||
animation = !TRANSITION_ANIMATION_DISABLED && animation;
|
||||
boolean animation = !TRANSITION_ANIMATION_DISABLED && animation;
|
||||
|
||||
switch (state) {
|
||||
case STATE_NORMAL:
|
||||
case STATE_NORMAL -> {
|
||||
// Show mMainView
|
||||
mViewTransition.showView(0, animation);
|
||||
// Show mBelowHeader
|
||||
mViewTransition2.showView(0, animation);
|
||||
break;
|
||||
case STATE_REFRESH:
|
||||
}
|
||||
case STATE_REFRESH ->
|
||||
// Show mProgressView
|
||||
mViewTransition.showView(1, animation);
|
||||
break;
|
||||
case STATE_REFRESH_HEADER:
|
||||
mViewTransition.showView(1, animation);
|
||||
case STATE_REFRESH_HEADER -> {
|
||||
// Show mMainView
|
||||
mViewTransition.showView(0, animation);
|
||||
// Show mProgress
|
||||
mViewTransition2.showView(1, animation);
|
||||
break;
|
||||
default:
|
||||
case STATE_INIT:
|
||||
case STATE_FAILED:
|
||||
}
|
||||
case STATE_INIT, STATE_FAILED ->
|
||||
// Show mFailedView
|
||||
mViewTransition.showView(2, animation);
|
||||
break;
|
||||
mViewTransition.showView(2, animation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,7 +871,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
private void bindTags(GalleryTagGroup[] tagGroups) {
|
||||
Context context = getContext();
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
if (null == context || null == inflater || null == mTags || null == mNoTags) {
|
||||
if (null == context || null == mTags || null == mNoTags) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -927,7 +883,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
mNoTags.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
EhTagDatabase ehTags = Settings.getShowTagTranslations() ? EhTagDatabase.getInstance(context) : null;
|
||||
EhTagDatabase ehTags = Settings.getShowTagTranslations() && EhTagDatabase.INSTANCE.isTranslatable(context) ? EhTagDatabase.INSTANCE : null;
|
||||
int colorTag = ResourcesKt.resolveColor(getTheme(), R.attr.tagBackgroundColor);
|
||||
int colorName = ResourcesKt.resolveColor(getTheme(), R.attr.tagGroupBackgroundColor);
|
||||
for (GalleryTagGroup tg : tagGroups) {
|
||||
@@ -936,8 +892,8 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
mTags.addView(ll);
|
||||
|
||||
String readableTagName = null;
|
||||
if (ehTags != null) {
|
||||
readableTagName = ehTags.getTranslation("n:" + tg.groupName);
|
||||
if (ehTags != null && ehTags.isInitialized()) {
|
||||
readableTagName = ehTags.getTranslation("n", tg.groupName);
|
||||
}
|
||||
|
||||
TextView tgName = (TextView) inflater.inflate(R.layout.item_gallery_tag, ll, false);
|
||||
@@ -946,9 +902,6 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
tgName.setBackgroundTintList(ColorStateList.valueOf(colorName));
|
||||
|
||||
String prefix = EhTagDatabase.namespaceToPrefix(tg.groupName);
|
||||
if (prefix == null) {
|
||||
prefix = "";
|
||||
}
|
||||
|
||||
AutoWrapLayout awl = new AutoWrapLayout(context);
|
||||
ll.addView(awl, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -959,7 +912,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
|
||||
String readableTag = null;
|
||||
if (ehTags != null) {
|
||||
readableTag = ehTags.getTranslation(prefix + tagStr);
|
||||
readableTag = ehTags.getTranslation(prefix, tagStr);
|
||||
}
|
||||
|
||||
tag.setText(readableTag != null ? readableTag : tagStr);
|
||||
@@ -974,7 +927,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
private void bindComments(GalleryComment[] comments) {
|
||||
Context context = getContext();
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
if (null == context || null == inflater || null == mComments || null == mCommentsText) {
|
||||
if (null == context || null == mComments || null == mCommentsText) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1013,7 +966,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
Resources resources = getResourcesOrNull();
|
||||
int previewNum = Settings.getPreviewNum();
|
||||
if (null == inflater || null == resources || null == mGridLayout || null == mPreviewText) {
|
||||
if (null == resources || null == mGridLayout || null == mPreviewText) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1049,16 +1002,6 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
return getString(R.string.rating_text, getString(getRatingText(rating)), rating, ratingCount);
|
||||
}
|
||||
|
||||
private void setTransitionName() {
|
||||
long gid = getGid();
|
||||
|
||||
if (gid != -1 && mThumb != null && mUploader != null && mCategory != null) {
|
||||
ViewCompat.setTransitionName(mThumb, TransitionNameFactory.getThumbTransitionName(gid));
|
||||
ViewCompat.setTransitionName(mUploader, TransitionNameFactory.getUploaderTransitionName(gid));
|
||||
ViewCompat.setTransitionName(mCategory, TransitionNameFactory.getCategoryTransitionName(gid));
|
||||
}
|
||||
}
|
||||
|
||||
private void showSimilarGalleryList() {
|
||||
GalleryDetail gd = mGalleryDetail;
|
||||
if (null == gd) {
|
||||
@@ -1120,7 +1063,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
|
||||
if (mTip == v) {
|
||||
if (request()) {
|
||||
adjustViewVisibility(STATE_REFRESH, true);
|
||||
adjustViewVisibility(STATE_REFRESH);
|
||||
}
|
||||
} else if (mUploader == v) {
|
||||
String uploader = getUploader();
|
||||
@@ -1191,17 +1134,17 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
var galleryInfoBottomSheet = new GalleryInfoBottomSheet(mGalleryDetail);
|
||||
galleryInfoBottomSheet.show(requireActivity().getSupportFragmentManager(), GalleryInfoBottomSheet.TAG);
|
||||
} else if (mHeart == v || mHeartOutline == v) {
|
||||
if (mGalleryDetail != null && !mModifingFavorites) {
|
||||
if (mGalleryDetail != null && !mModifyingFavorites) {
|
||||
boolean remove = false;
|
||||
if (EhDB.containLocalFavorites(mGalleryDetail.gid) || mGalleryDetail.isFavorited) {
|
||||
mModifingFavorites = true;
|
||||
mModifyingFavorites = true;
|
||||
CommonOperations.removeFromFavorites(activity, mGalleryDetail,
|
||||
new ModifyFavoritesListener(context,
|
||||
activity.getStageId(), getTag(), true));
|
||||
remove = true;
|
||||
}
|
||||
if (!remove) {
|
||||
mModifingFavorites = true;
|
||||
mModifyingFavorites = true;
|
||||
CommonOperations.addToFavorites(activity, mGalleryDetail,
|
||||
new ModifyFavoritesListener(context,
|
||||
activity.getStageId(), getTag(), false));
|
||||
@@ -1210,7 +1153,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
updateFavoriteDrawable();
|
||||
}
|
||||
} else if (mShare == v) {
|
||||
String url = getGalleryDetailUrl(false);
|
||||
String url = getGalleryDetailUrl();
|
||||
if (url != null) {
|
||||
AppHelper.share(activity, url);
|
||||
}
|
||||
@@ -1368,7 +1311,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
|
||||
menu.add(resources.getString(android.R.string.copy));
|
||||
menuId.add(R.id.copy);
|
||||
if (!tag.equals(tv.getText().toString())) {
|
||||
if (!tag2.equals(tv.getText().toString())) {
|
||||
menu.add(resources.getString(R.string.copy_trans));
|
||||
menuId.add(R.id.copy_trans);
|
||||
}
|
||||
@@ -1440,17 +1383,17 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
}
|
||||
return true;
|
||||
} else if (mHeart == v || mHeartOutline == v) {
|
||||
if (mGalleryDetail != null && !mModifingFavorites) {
|
||||
if (mGalleryDetail != null && !mModifyingFavorites) {
|
||||
boolean remove = false;
|
||||
if (EhDB.containLocalFavorites(mGalleryDetail.gid) || mGalleryDetail.isFavorited) {
|
||||
mModifingFavorites = true;
|
||||
mModifyingFavorites = true;
|
||||
CommonOperations.removeFromFavorites(activity, mGalleryDetail,
|
||||
new ModifyFavoritesListener(activity,
|
||||
activity.getStageId(), getTag(), true));
|
||||
remove = true;
|
||||
}
|
||||
if (!remove) {
|
||||
mModifingFavorites = true;
|
||||
mModifyingFavorites = true;
|
||||
CommonOperations.addToFavorites(activity, mGalleryDetail,
|
||||
new ModifyFavoritesListener(activity,
|
||||
activity.getStageId(), getTag(), false), true);
|
||||
@@ -1495,25 +1438,13 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
return;
|
||||
}
|
||||
switch (mDownloadState) {
|
||||
default:
|
||||
case DownloadInfo.STATE_INVALID:
|
||||
mDownload.setText(R.string.download);
|
||||
break;
|
||||
case DownloadInfo.STATE_NONE:
|
||||
mDownload.setText(R.string.download_state_none);
|
||||
break;
|
||||
case DownloadInfo.STATE_WAIT:
|
||||
mDownload.setText(R.string.download_state_wait);
|
||||
break;
|
||||
case DownloadInfo.STATE_DOWNLOAD:
|
||||
mDownload.setText(R.string.download_state_downloading);
|
||||
break;
|
||||
case DownloadInfo.STATE_FINISH:
|
||||
mDownload.setText(R.string.download_state_downloaded);
|
||||
break;
|
||||
case DownloadInfo.STATE_FAILED:
|
||||
mDownload.setText(R.string.download_state_failed);
|
||||
break;
|
||||
case DownloadInfo.STATE_INVALID -> mDownload.setText(R.string.download);
|
||||
case DownloadInfo.STATE_NONE -> mDownload.setText(R.string.download_state_none);
|
||||
case DownloadInfo.STATE_WAIT -> mDownload.setText(R.string.download_state_wait);
|
||||
case DownloadInfo.STATE_DOWNLOAD ->
|
||||
mDownload.setText(R.string.download_state_downloading);
|
||||
case DownloadInfo.STATE_FINISH -> mDownload.setText(R.string.download_state_downloaded);
|
||||
case DownloadInfo.STATE_FAILED -> mDownload.setText(R.string.download_state_failed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1573,7 +1504,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
private void onGetGalleryDetailSuccess(GalleryDetail result) {
|
||||
mGalleryDetail = result;
|
||||
updateDownloadState();
|
||||
adjustViewVisibility(STATE_NORMAL, true);
|
||||
adjustViewVisibility(STATE_NORMAL);
|
||||
bindViewSecond();
|
||||
}
|
||||
|
||||
@@ -1583,7 +1514,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
if (null != context && null != mTip) {
|
||||
String error = ExceptionUtils.getReadableString(e);
|
||||
mTip.setText(error);
|
||||
adjustViewVisibility(STATE_FAILED, true);
|
||||
adjustViewVisibility(STATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1601,26 +1532,26 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
}
|
||||
|
||||
private void onModifyFavoritesSuccess(boolean addOrRemove) {
|
||||
mModifingFavorites = false;
|
||||
mModifyingFavorites = false;
|
||||
if (mGalleryDetail != null) {
|
||||
mGalleryDetail.isFavorited = !addOrRemove && mGalleryDetail.favoriteName != null;
|
||||
updateFavoriteDrawable();
|
||||
}
|
||||
}
|
||||
|
||||
private void onModifyFavoritesFailure(boolean addOrRemove) {
|
||||
mModifingFavorites = false;
|
||||
private void onModifyFavoritesFailure() {
|
||||
mModifyingFavorites = false;
|
||||
}
|
||||
|
||||
private void onModifyFavoritesCancel(boolean addOrRemove) {
|
||||
mModifingFavorites = false;
|
||||
private void onModifyFavoritesCancel() {
|
||||
mModifyingFavorites = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent outContent) {
|
||||
super.onProvideAssistContent(outContent);
|
||||
|
||||
String url = getGalleryDetailUrl(false);
|
||||
String url = getGalleryDetailUrl();
|
||||
if (url != null) {
|
||||
outContent.setWebUri(Uri.parse(url));
|
||||
}
|
||||
@@ -1774,7 +1705,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
R.string.add_to_favorite_failure, LENGTH_LONG);
|
||||
GalleryDetailScene scene = getScene();
|
||||
if (scene != null) {
|
||||
scene.onModifyFavoritesFailure(mAddOrRemove);
|
||||
scene.onModifyFavoritesFailure();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,7 +1713,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
public void onCancel() {
|
||||
GalleryDetailScene scene = getScene();
|
||||
if (scene != null) {
|
||||
scene.onModifyFavoritesCancel(mAddOrRemove);
|
||||
scene.onModifyFavoritesCancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2108,7 +2039,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteDialogHelper implements DialogInterface.OnClickListener {
|
||||
private static class DeleteDialogHelper implements DialogInterface.OnClickListener {
|
||||
private final com.hippo.ehviewer.download.DownloadManager mDownloadManager;
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
private final CheckBoxDialogBuilder mBuilder;
|
||||
|
||||
@@ -48,6 +48,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
@@ -150,7 +151,7 @@ public final class GalleryListScene extends SearchBarScene
|
||||
private EasyRecyclerView mRecyclerView;
|
||||
@Nullable
|
||||
private SearchLayout mSearchLayout;
|
||||
ActivityResultLauncher<PickVisualMediaRequest> selectImageLauncher = registerForActivityResult(
|
||||
final ActivityResultLauncher<PickVisualMediaRequest> selectImageLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.PickVisualMedia(),
|
||||
result -> mSearchLayout.setImageUri(result));
|
||||
@Nullable
|
||||
@@ -467,30 +468,33 @@ public final class GalleryListScene extends SearchBarScene
|
||||
private void onUpdateUrlBuilder() {
|
||||
ListUrlBuilder builder = mUrlBuilder;
|
||||
Resources resources = getResourcesOrNull();
|
||||
if (resources == null || builder == null || mSearchLayout == null) {
|
||||
if (resources == null || builder == null || mSearchLayout == null || mFabLayout == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String keyword = builder.getKeyword();
|
||||
int category = builder.getCategory();
|
||||
|
||||
boolean isTopList = builder.getMode() == ListUrlBuilder.MODE_TOPLIST;
|
||||
var mode = builder.getMode();
|
||||
boolean isPopular = mode == ListUrlBuilder.MODE_WHATS_HOT;
|
||||
boolean isTopList = mode == ListUrlBuilder.MODE_TOPLIST;
|
||||
if (isTopList != mIsTopList) {
|
||||
mIsTopList = isTopList;
|
||||
recreateDrawerView();
|
||||
mFabLayout.getSecondaryFabAt(0).setImageResource(isTopList ? R.drawable.ic_baseline_format_list_numbered_24 : R.drawable.v_magnify_x24);
|
||||
}
|
||||
|
||||
mFabLayout.setSecondaryFabVisibilityAt(1, ListUrlBuilder.MODE_WHATS_HOT != builder.getMode());
|
||||
// Update fab visibility
|
||||
mFabLayout.setSecondaryFabVisibilityAt(0, !isPopular);
|
||||
mFabLayout.setSecondaryFabVisibilityAt(2, !isPopular);
|
||||
|
||||
// Update normal search mode
|
||||
mSearchLayout.setNormalSearchMode(builder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION
|
||||
mSearchLayout.setNormalSearchMode(mode == ListUrlBuilder.MODE_SUBSCRIPTION
|
||||
? R.id.search_subscription_search
|
||||
: R.id.search_normal_search);
|
||||
|
||||
// Update search edit text
|
||||
if (!TextUtils.isEmpty(keyword) && !mIsTopList) {
|
||||
if (builder.getMode() == ListUrlBuilder.MODE_TAG) {
|
||||
if (mode == ListUrlBuilder.MODE_TAG) {
|
||||
keyword = wrapTagKeyword(keyword);
|
||||
}
|
||||
setSearchBarText(keyword);
|
||||
@@ -505,19 +509,16 @@ public final class GalleryListScene extends SearchBarScene
|
||||
|
||||
// Update nav checked item
|
||||
int checkedItemId;
|
||||
if (ListUrlBuilder.MODE_NORMAL == builder.getMode() &&
|
||||
EhUtils.NONE == category &&
|
||||
TextUtils.isEmpty(keyword)) {
|
||||
checkedItemId = R.id.nav_homepage;
|
||||
} else if (ListUrlBuilder.MODE_SUBSCRIPTION == builder.getMode()) {
|
||||
checkedItemId = R.id.nav_subscription;
|
||||
} else if (ListUrlBuilder.MODE_WHATS_HOT == builder.getMode()) {
|
||||
checkedItemId = R.id.nav_whats_hot;
|
||||
} else if (ListUrlBuilder.MODE_TOPLIST == builder.getMode()) {
|
||||
checkedItemId = R.id.nav_toplist;
|
||||
} else {
|
||||
checkedItemId = 0;
|
||||
}
|
||||
checkedItemId = switch (mode) {
|
||||
case ListUrlBuilder.MODE_NORMAL ->
|
||||
EhUtils.NONE == category && TextUtils.isEmpty(keyword) ? R.id.nav_homepage : 0;
|
||||
case ListUrlBuilder.MODE_SUBSCRIPTION -> R.id.nav_subscription;
|
||||
case ListUrlBuilder.MODE_WHATS_HOT -> R.id.nav_whats_hot;
|
||||
case ListUrlBuilder.MODE_TOPLIST -> R.id.nav_toplist;
|
||||
case ListUrlBuilder.MODE_TAG, ListUrlBuilder.MODE_UPLOADER, ListUrlBuilder.MODE_IMAGE_SEARCH ->
|
||||
0;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + mode);
|
||||
};
|
||||
setNavCheckedItem(checkedItemId);
|
||||
mNavCheckedId = checkedItemId;
|
||||
}
|
||||
@@ -541,6 +542,7 @@ public final class GalleryListScene extends SearchBarScene
|
||||
FastScroller fastScroller = mContentLayout.getFastScroller();
|
||||
mSearchLayout = (SearchLayout) ViewUtils.$$(mainLayout, R.id.search_layout);
|
||||
mFabLayout = (FabLayout) ViewUtils.$$(mainLayout, R.id.fab_layout);
|
||||
AssertUtils.assertNotNull(container);
|
||||
mSearchFab = ViewUtils.$$(container, R.id.search_fab);
|
||||
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
|
||||
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
|
||||
@@ -656,7 +658,7 @@ public final class GalleryListScene extends SearchBarScene
|
||||
final EasyRecyclerView recyclerView, final TextView tip) {
|
||||
Context context = getContext();
|
||||
final ListUrlBuilder urlBuilder = mUrlBuilder;
|
||||
if (null == context || null == urlBuilder) {
|
||||
if (null == context || null == urlBuilder || null == mHelper) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -666,11 +668,17 @@ public final class GalleryListScene extends SearchBarScene
|
||||
return;
|
||||
}
|
||||
|
||||
var gi = mHelper.getFirstVisibleItem();
|
||||
var next = gi != null ? "@" + (gi.gid + 1) : null;
|
||||
|
||||
// Check duplicate
|
||||
for (QuickSearch q : mQuickSearchList) {
|
||||
if (urlBuilder.equalsQuickSearch(q)) {
|
||||
showTip(getString(R.string.duplicate_quick_search, q.name), LENGTH_LONG);
|
||||
return;
|
||||
var i = q.name.lastIndexOf("@");
|
||||
if (i != -1 && q.name.substring(i).equals(next)) {
|
||||
showTip(getString(R.string.duplicate_quick_search, q.name), LENGTH_LONG);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +686,10 @@ public final class GalleryListScene extends SearchBarScene
|
||||
getSuitableTitleForUrlBuilder(context.getResources(), urlBuilder, false), getString(R.string.quick_search));
|
||||
builder.setTitle(R.string.add_quick_search_dialog_title);
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
// TODO: It's ugly
|
||||
final boolean[] checked = {Settings.getQSSaveProgress()};
|
||||
final String[] hint = {getString(R.string.save_progress)};
|
||||
builder.setMultiChoiceItems(hint, checked, (dialog, which, isChecked) -> checked[which] = isChecked);
|
||||
final AlertDialog dialog = builder.show();
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
String text = builder.getText().trim();
|
||||
@@ -688,6 +700,13 @@ public final class GalleryListScene extends SearchBarScene
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked[0] && next != null) {
|
||||
text += next;
|
||||
Settings.putQSSaveProgress(true);
|
||||
} else {
|
||||
Settings.putQSSaveProgress(false);
|
||||
}
|
||||
|
||||
// Check name duplicate
|
||||
for (QuickSearch q : mQuickSearchList) {
|
||||
if (text.equals(q.name)) {
|
||||
@@ -764,17 +783,11 @@ public final class GalleryListScene extends SearchBarScene
|
||||
if (null != mFabLayout && mFabLayout.isExpanded()) {
|
||||
return false;
|
||||
}
|
||||
if (isSearchViewShown())
|
||||
return false;
|
||||
return mState == STATE_NORMAL && getStackIndex() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isSearchViewShown()) {
|
||||
hideSearchView();
|
||||
return;
|
||||
}
|
||||
if (null != mFabLayout && mFabLayout.isExpanded()) {
|
||||
mFabLayout.setExpanded(false);
|
||||
return;
|
||||
@@ -797,14 +810,14 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onItemClick(int position) {
|
||||
public void onItemClick(int position) {
|
||||
if (null == mHelper || null == mRecyclerView) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
GalleryInfo gi = mHelper.getDataAtEx(position);
|
||||
if (gi == null) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
@@ -812,7 +825,6 @@ public final class GalleryListScene extends SearchBarScene
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi);
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
startScene(announcer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -824,13 +836,13 @@ public final class GalleryListScene extends SearchBarScene
|
||||
|
||||
private void showGoToDialog() {
|
||||
Context context = getContext();
|
||||
if (null == context || null == mHelper) {
|
||||
if (null == context || null == mHelper || null == mUrlBuilder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ListUrlBuilder.MODE_TOPLIST == mUrlBuilder.getMode()) {
|
||||
if (mIsTopList) {
|
||||
final int page = mHelper.getPageForTop() + 1;
|
||||
final int pages = 200;
|
||||
final int pages = mHelper.getPages();
|
||||
String hint = getString(R.string.go_to_hint, page, pages);
|
||||
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
|
||||
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
@@ -856,7 +868,7 @@ public final class GalleryListScene extends SearchBarScene
|
||||
return;
|
||||
}
|
||||
builder.setError(null);
|
||||
mHelper.goToPage(goTo);
|
||||
mHelper.goTo(goTo);
|
||||
dialog.dismiss();
|
||||
});
|
||||
} else {
|
||||
@@ -879,7 +891,7 @@ public final class GalleryListScene extends SearchBarScene
|
||||
.setSelection(toDate)
|
||||
.build();
|
||||
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
|
||||
datePicker.addOnPositiveButtonClickListener(v -> mHelper.goTo(v));
|
||||
datePicker.addOnPositiveButtonClickListener(v -> mHelper.goTo(v, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,17 +902,20 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case 0: // Open right
|
||||
openDrawer(Gravity.RIGHT);
|
||||
break;
|
||||
case 1: // Go to
|
||||
if (mHelper.canGoTo()) {
|
||||
showGoToDialog();
|
||||
// Go to
|
||||
case 0 -> {
|
||||
if (mHelper.canGoTo()) showGoToDialog();
|
||||
}
|
||||
// Refresh
|
||||
case 1 -> mHelper.refresh();
|
||||
// Last page
|
||||
case 2 -> {
|
||||
if (mIsTopList) {
|
||||
mHelper.goTo(mHelper.getPages() - 1);
|
||||
} else {
|
||||
mHelper.goTo("1", false);
|
||||
}
|
||||
break;
|
||||
case 2: // Refresh
|
||||
mHelper.refresh();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
view.setExpanded(false);
|
||||
@@ -913,12 +928,12 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
mActionFabDrawable.setDelete(ANIMATE_TIME);
|
||||
} else {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
mActionFabDrawable.setAdd(ANIMATE_TIME);
|
||||
}
|
||||
}
|
||||
@@ -1217,23 +1232,23 @@ public final class GalleryListScene extends SearchBarScene
|
||||
@Override
|
||||
public void onStartDragHandler() {
|
||||
// Lock right drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndDragHandler() {
|
||||
// Restore right drawer
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
showSearchBar();
|
||||
}
|
||||
|
||||
public void onStateChange(int newState) {
|
||||
if (newState == STATE_NORMAL || newState == STATE_SIMPLE_SEARCH) {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END);
|
||||
} else {
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.LEFT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,21 +1271,16 @@ public final class GalleryListScene extends SearchBarScene
|
||||
|
||||
|
||||
private void onGetGalleryListSuccess(GalleryListParser.Result result, int taskId) {
|
||||
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
|
||||
if (mHelper != null && mHelper.isCurrentTask(taskId) && mUrlBuilder != null) {
|
||||
String emptyString = getString(mUrlBuilder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION && result.noWatchedTags
|
||||
? R.string.gallery_list_empty_hit_subscription
|
||||
: R.string.gallery_list_empty_hit);
|
||||
mHelper.setEmptyString(emptyString);
|
||||
|
||||
int pages;
|
||||
if (mIsTopList)
|
||||
pages = 200;
|
||||
else if (result.nextGid == 0)
|
||||
pages = mHelper.pgCounter + 1;
|
||||
else
|
||||
pages = result.founds;
|
||||
|
||||
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
|
||||
if (mIsTopList) {
|
||||
mHelper.onGetPageData(taskId, result.pages, result.nextPage, null, null, result.galleryInfoList);
|
||||
} else {
|
||||
mHelper.onGetPageData(taskId, 0, 0, result.prev, result.next, result.galleryInfoList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,12 +1451,13 @@ public final class GalleryListScene extends SearchBarScene
|
||||
return;
|
||||
}
|
||||
|
||||
mUrlBuilder.set(mQuickSearchList.get(position));
|
||||
mUrlBuilder.setNextGid(0);
|
||||
var q = mQuickSearchList.get(position);
|
||||
mUrlBuilder.set(q);
|
||||
onUpdateUrlBuilder();
|
||||
mHelper.refresh();
|
||||
var i = q.name.lastIndexOf("@");
|
||||
mHelper.goTo(i != -1 ? q.name.substring(i + 1) : null, true);
|
||||
setState(STATE_NORMAL);
|
||||
closeDrawer(Gravity.RIGHT);
|
||||
closeDrawer(GravityCompat.END);
|
||||
});
|
||||
} else {
|
||||
int[] keywords = {11, 12, 13, 15};
|
||||
@@ -1459,11 +1470,10 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
|
||||
mUrlBuilder.setKeyword(String.valueOf(keywords[position]));
|
||||
mUrlBuilder.setNextGid(0);
|
||||
onUpdateUrlBuilder();
|
||||
mHelper.refresh();
|
||||
setState(STATE_NORMAL);
|
||||
closeDrawer(Gravity.RIGHT);
|
||||
closeDrawer(GravityCompat.END);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1589,38 +1599,20 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
|
||||
private class GalleryListHelper extends GalleryInfoContentHelper {
|
||||
public int pgCounter = 0;
|
||||
|
||||
@Override
|
||||
protected void getPageData(int taskId, int type, int page) {
|
||||
pgCounter = page;
|
||||
protected void getPageData(int taskId, int type, int page, String index, boolean isNext) {
|
||||
MainActivity activity = getMainActivity();
|
||||
if (null == activity || null == mClient || null == mUrlBuilder) {
|
||||
if (null == activity || null == mClient || null == mHelper || null == mUrlBuilder) {
|
||||
return;
|
||||
}
|
||||
|
||||
int prevGid = 0, nextGid = 0;
|
||||
if (mIsTopList) {
|
||||
if (jumpTo != null) {
|
||||
pgCounter = Integer.parseInt(jumpTo);
|
||||
nextGid = pgCounter;
|
||||
} else {
|
||||
nextGid = page;
|
||||
}
|
||||
} else if (jumpTo != null) {
|
||||
nextGid = minGid;
|
||||
} else if (page != 0) {
|
||||
if (page >= mHelper.getPageForTop())
|
||||
nextGid = minGid;
|
||||
else
|
||||
prevGid = maxGid;
|
||||
mUrlBuilder.setJumpTo(String.valueOf(page));
|
||||
} else {
|
||||
mUrlBuilder.setIndex(index, isNext);
|
||||
mUrlBuilder.setJumpTo(jumpTo);
|
||||
}
|
||||
mUrlBuilder.setPrevGid(prevGid);
|
||||
mUrlBuilder.setNextGid(nextGid);
|
||||
|
||||
mUrlBuilder.setJumpTo(jumpTo);
|
||||
jumpTo = null;
|
||||
|
||||
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) {
|
||||
EhRequest request = new EhRequest();
|
||||
request.setMethod(EhClient.METHOD_IMAGE_SEARCH);
|
||||
@@ -1654,13 +1646,6 @@ public final class GalleryListScene extends SearchBarScene
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeChanged(int positionStart, int itemCount) {
|
||||
if (null != mAdapter) {
|
||||
mAdapter.notifyItemRangeChanged(positionStart, itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (null != mAdapter) {
|
||||
|
||||
@@ -247,7 +247,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
list.add(previewSet.getGalleryPreview(mGalleryInfo.gid, i));
|
||||
}
|
||||
|
||||
mHelper.onGetPageData(taskId, result.second, 0, list);
|
||||
mHelper.onGetPageData(taskId, result.second, 0, null, null, list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
private class GalleryPreviewHelper extends ContentLayout.ContentHelper<GalleryPreview> {
|
||||
|
||||
@Override
|
||||
protected void getPageData(final int taskId, int type, int page) {
|
||||
protected void getPageData(final int taskId, int type, int page, String index, boolean isNext) {
|
||||
MainActivity activity = getMainActivity();
|
||||
if (null == activity || null == mClient || null == mGalleryInfo) {
|
||||
onGetException(taskId, new EhException(getString(R.string.error_cannot_find_gallery)));
|
||||
@@ -372,13 +372,6 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeChanged(int positionStart, int itemCount) {
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyItemRangeChanged(positionStart, itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (mAdapter != null) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
@@ -190,7 +191,7 @@ class HistoryScene : ToolbarScene() {
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun onNavigationClick() {
|
||||
toggleDrawer(Gravity.LEFT)
|
||||
toggleDrawer(GravityCompat.START)
|
||||
}
|
||||
|
||||
override fun getMenuResId(): Int {
|
||||
|
||||
@@ -9,18 +9,29 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.search.SearchBar
|
||||
import com.google.android.material.search.SearchView
|
||||
import com.google.android.material.search.SearchView.TransitionListener
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.widget.SearchDatabase
|
||||
import com.hippo.scene.StageActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
abstract class SearchBarScene : ToolbarScene() {
|
||||
@@ -66,18 +77,46 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
else if (newState == SearchView.TransitionState.HIDING)
|
||||
onSearchViewHidden()
|
||||
}
|
||||
mSearchView?.addTransitionListener(mSearchViewOnBackPressedCallback)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(mSearchViewOnBackPressedCallback)
|
||||
val contentView = onCreateViewWithToolbar(inflater, view, savedInstanceState)
|
||||
return view.apply { addView(contentView, 0) }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mSearchViewOnBackPressedCallback.remove()
|
||||
}
|
||||
|
||||
private var privLockModeStart: Int? = null
|
||||
private var privLockModeEnd: Int? = null
|
||||
|
||||
@CallSuper
|
||||
open fun onSearchViewExpanded() {
|
||||
privLockModeStart = getDrawerLockMode(GravityCompat.START)
|
||||
privLockModeEnd = getDrawerLockMode(GravityCompat.END)
|
||||
privLockModeStart?.let {
|
||||
setDrawerLockMode(
|
||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
|
||||
GravityCompat.START
|
||||
)
|
||||
}
|
||||
privLockModeEnd?.let {
|
||||
setDrawerLockMode(
|
||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
|
||||
GravityCompat.END
|
||||
)
|
||||
}
|
||||
(requireActivity() as StageActivity).updateBackPressCallBackStatus()
|
||||
updateSuggestions()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onSearchViewHidden() {
|
||||
privLockModeStart?.let { setDrawerLockMode(it, GravityCompat.START) }
|
||||
privLockModeStart = null
|
||||
privLockModeEnd?.let { setDrawerLockMode(it, GravityCompat.END) }
|
||||
privLockModeEnd = null
|
||||
(requireActivity() as StageActivity).updateBackPressCallBackStatus()
|
||||
}
|
||||
|
||||
@@ -110,7 +149,7 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
}
|
||||
|
||||
fun onApplySearch() {
|
||||
val query = mSearchView?.text.toString().trim()
|
||||
val query = (mToolbar as SearchBar).text.toString().trim()
|
||||
if (!mAllowEmptySearch && query.isEmpty()) {
|
||||
return
|
||||
}
|
||||
@@ -122,14 +161,6 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
mAllowEmptySearch = allowEmptySearch
|
||||
}
|
||||
|
||||
fun isSearchViewShown(): Boolean {
|
||||
return mSearchView?.isShowing ?: false
|
||||
}
|
||||
|
||||
fun hideSearchView() {
|
||||
mSearchView?.hide()
|
||||
}
|
||||
|
||||
private fun wrapTagKeyword(keyword: String): String {
|
||||
var keyword = keyword
|
||||
keyword = keyword.trim { it <= ' ' }
|
||||
@@ -220,10 +251,13 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
|
||||
}
|
||||
|
||||
inner class TagSuggestion constructor(private var mHint: String, private var mKeyword: String) :
|
||||
inner class TagSuggestion constructor(
|
||||
private var mHint: String?,
|
||||
private var mKeyword: String
|
||||
) :
|
||||
Suggestion() {
|
||||
|
||||
override fun getText(textView: TextView): CharSequence {
|
||||
override fun getText(textView: TextView): CharSequence? {
|
||||
return if (textView.id == android.R.id.text1) {
|
||||
mKeyword
|
||||
} else {
|
||||
@@ -240,7 +274,7 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
temp = text.substring(0, text.lastIndexOf(" ")) + " " + temp
|
||||
}
|
||||
it.setText(temp)
|
||||
it.setSelection(text.length)
|
||||
it.setSelection(temp.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,32 +311,44 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun updateSuggestions(scrollToTop: Boolean = true) {
|
||||
val suggestions = arrayListOf<Suggestion>()
|
||||
val text = mSearchView?.editText?.text?.toString() ?: return
|
||||
mSuggestionProvider?.run {
|
||||
providerSuggestions(text)?.let { suggestions.addAll(it) }
|
||||
}
|
||||
mSearchDatabase.getSuggestions(text, 128).forEach { suggestions.add(KeywordSuggestion(it)) }
|
||||
val ehTagDatabase = EhTagDatabase.getInstance(context)
|
||||
if (!TextUtils.isEmpty(text) && ehTagDatabase != null && !text.endsWith(" ")) {
|
||||
val s = text.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (s.isNotEmpty()) {
|
||||
val keyword = s[s.size - 1]
|
||||
val searchHints = ehTagDatabase.suggest(keyword)
|
||||
|
||||
for (searchHint in searchHints) {
|
||||
suggestions.add(TagSuggestion(searchHint.first, searchHint.second))
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
|
||||
val suggestions = mutableListOf<Suggestion>()
|
||||
mergedSuggestionFlow().collect {
|
||||
suggestions.add(it)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
mSuggestionList = suggestions
|
||||
mSuggestionAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
mSuggestionList = suggestions
|
||||
mSuggestionAdapter?.notifyDataSetChanged()
|
||||
if (scrollToTop) {
|
||||
mRecyclerView?.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergedSuggestionFlow(): Flow<Suggestion> = flow {
|
||||
mSearchView?.editText?.text?.toString()?.let { text ->
|
||||
mSuggestionProvider?.run { providerSuggestions(text)?.forEach { emit(it) } }
|
||||
mSearchDatabase.getSuggestions(text, 128).forEach { emit(KeywordSuggestion(it)) }
|
||||
EhTagDatabase.takeIf { it.isInitialized() }?.run {
|
||||
if (!TextUtils.isEmpty(text) && !text.endsWith(" ")) {
|
||||
val s = text.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (s.isNotEmpty()) {
|
||||
val keyword = s[s.size - 1]
|
||||
val translate =
|
||||
Settings.getShowTagTranslations() && isTranslatable(requireContext())
|
||||
suggestFlow(keyword, translate, true).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
suggestFlow(keyword, translate).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSuggestionProvider(suggestionProvider: SuggestionProvider) {
|
||||
mSuggestionProvider = suggestionProvider
|
||||
}
|
||||
@@ -310,4 +356,22 @@ abstract class SearchBarScene : ToolbarScene() {
|
||||
fun showSearchBar() {
|
||||
mAppBarLayout?.setExpanded(true)
|
||||
}
|
||||
|
||||
private val mSearchViewOnBackPressedCallback =
|
||||
object : OnBackPressedCallback(false), TransitionListener {
|
||||
override fun handleOnBackPressed() {
|
||||
mSearchView?.hide()
|
||||
}
|
||||
|
||||
override fun onStateChanged(
|
||||
searchView: SearchView,
|
||||
previousState: SearchView.TransitionState,
|
||||
newState: SearchView.TransitionState
|
||||
) {
|
||||
if (newState == SearchView.TransitionState.SHOWING)
|
||||
isEnabled = true
|
||||
else if (newState == SearchView.TransitionState.HIDING)
|
||||
isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,16 +27,8 @@ import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
|
||||
@JvmField
|
||||
var maxGid = -1
|
||||
|
||||
@JvmField
|
||||
var minGid = -1
|
||||
|
||||
@JvmField
|
||||
var jumpTo: String? = null
|
||||
private val listener: FavouriteStatusRouter.Listener
|
||||
@@ -58,26 +50,14 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
|
||||
EhApplication.favouriteStatusRouter.removeListener(listener)
|
||||
}
|
||||
|
||||
override fun onAddData(data: GalleryInfo?) {
|
||||
data?.let { map[data.gid] = data }
|
||||
}
|
||||
|
||||
override fun onAddData(data: List<GalleryInfo?>) {
|
||||
for (info in data) {
|
||||
info?.let {
|
||||
if (maxGid == -1) maxGid = info.gid.toInt()
|
||||
if (minGid == -1) minGid = info.gid.toInt()
|
||||
maxGid = max(maxGid.toLong(), info.gid).toInt()
|
||||
minGid = min(minGid.toLong(), info.gid).toInt()
|
||||
map[info.gid] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveData(data: GalleryInfo?) {
|
||||
data?.let { map.remove(data.gid) }
|
||||
}
|
||||
|
||||
override fun onRemoveData(data: List<GalleryInfo?>) {
|
||||
for (info in data) {
|
||||
info?.let { map.remove(info.gid) }
|
||||
@@ -86,14 +66,6 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
|
||||
|
||||
override fun onClearData() {
|
||||
map.clear()
|
||||
maxGid = -1
|
||||
minGid = -1
|
||||
}
|
||||
|
||||
override fun beforeRefresh() {
|
||||
super.beforeRefresh()
|
||||
maxGid = -1
|
||||
minGid = -1
|
||||
}
|
||||
|
||||
override fun saveInstanceState(superState: Parcelable): Parcelable {
|
||||
@@ -103,8 +75,6 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
|
||||
val router = EhApplication.favouriteStatusRouter
|
||||
val id = router.saveDataMap(map)
|
||||
bundle.putInt(KEY_DATA_MAP, id)
|
||||
bundle.putInt(KEY_MIN_GID, minGid)
|
||||
bundle.putInt(KEY_MAX_GID, maxGid)
|
||||
return bundle
|
||||
}
|
||||
|
||||
@@ -118,28 +88,22 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
|
||||
this.map = map
|
||||
}
|
||||
}
|
||||
minGid = bundle.getInt(KEY_MIN_GID)
|
||||
maxGid = bundle.getInt(KEY_MAX_GID)
|
||||
return super.restoreInstanceState(state)
|
||||
}
|
||||
|
||||
fun goTo(time: Long) {
|
||||
fun goTo(time: Long, isNext: Boolean) {
|
||||
val formatter = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
|
||||
jumpTo = formatter.format(Instant.ofEpochMilli(time))
|
||||
doGoToPage(Int.MAX_VALUE / 2)
|
||||
}
|
||||
|
||||
fun goToPage(page: Int) {
|
||||
jumpTo = page.toString()
|
||||
doGoToPage(page)
|
||||
if (isNext) {
|
||||
goTo(mNext ?: "2", true)
|
||||
} else {
|
||||
goTo(mPrev, false)
|
||||
}
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_DATA_MAP = "data_map"
|
||||
private const val KEY_MAX_GID = "max_gid"
|
||||
private const val KEY_MIN_GID = "min_gid"
|
||||
const val KEY_UPPER_PAGE = "upper_page"
|
||||
const val KEY_LOWER_PAGE = "lower_page"
|
||||
}
|
||||
}
|
||||
@@ -207,8 +207,15 @@ class SearchLayout @JvmOverloads constructor(
|
||||
if (mEnableAdvance) {
|
||||
urlBuilder.advanceSearch = mTableAdvanceSearch.advanceSearch
|
||||
urlBuilder.minRating = mTableAdvanceSearch.minRating
|
||||
urlBuilder.pageFrom = mTableAdvanceSearch.pageFrom
|
||||
urlBuilder.pageTo = mTableAdvanceSearch.pageTo
|
||||
val pageFrom = mTableAdvanceSearch.pageFrom
|
||||
val pageTo = mTableAdvanceSearch.pageTo
|
||||
if (pageTo != -1 && pageTo < 10) {
|
||||
throw EhException(context.getString(R.string.search_sp_err1))
|
||||
} else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {
|
||||
throw EhException(context.getString(R.string.search_sp_err2))
|
||||
}
|
||||
urlBuilder.pageFrom = pageFrom
|
||||
urlBuilder.pageTo = pageTo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,74 +17,60 @@
|
||||
*/
|
||||
package com.hippo.image
|
||||
|
||||
import android.graphics.ColorSpace
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.ALLOCATOR_HARDWARE
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.graphics.ImageDecoder.ImageInfo
|
||||
import android.graphics.ImageDecoder.Source
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.hippo.UriArchiveAccessor
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class Image private constructor(source: Source?, val release: () -> Unit = {}) {
|
||||
class Image private constructor(source: Source, private val byteBuffer: ByteBuffer? = null) {
|
||||
var mObtainedDrawable: Drawable?
|
||||
|
||||
init {
|
||||
mObtainedDrawable = null
|
||||
source?.let {
|
||||
mObtainedDrawable =
|
||||
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, src: Source ->
|
||||
decoder.allocator = ALLOCATOR_HARDWARE
|
||||
decoder.setTargetSampleSize(
|
||||
max(
|
||||
min(
|
||||
info.size.width / (2 * screenWidth),
|
||||
info.size.height / (2 * screenHeight)
|
||||
), 1
|
||||
)
|
||||
)
|
||||
} // Should we lazy decode it?
|
||||
}
|
||||
mObtainedDrawable =
|
||||
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
|
||||
decoder.setTargetColorSpace(colorSpace)
|
||||
decoder.setTargetSampleSize(
|
||||
min(
|
||||
info.size.width / (2 * screenWidth),
|
||||
info.size.height / (2 * screenHeight)
|
||||
).coerceAtLeast(1)
|
||||
)
|
||||
}
|
||||
if (mObtainedDrawable is BitmapDrawable)
|
||||
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
|
||||
}
|
||||
|
||||
val width = mObtainedDrawable!!.intrinsicWidth
|
||||
val height = mObtainedDrawable!!.intrinsicHeight
|
||||
var started = false
|
||||
val width: Int
|
||||
get() = mObtainedDrawable?.intrinsicWidth ?: 0
|
||||
val height: Int
|
||||
get() = mObtainedDrawable?.intrinsicHeight ?: 0
|
||||
|
||||
@Synchronized
|
||||
fun recycle() {
|
||||
if (mObtainedDrawable is AnimatedImageDrawable) {
|
||||
(mObtainedDrawable as AnimatedImageDrawable?)?.stop()
|
||||
}
|
||||
if (mObtainedDrawable is BitmapDrawable) {
|
||||
(mObtainedDrawable as BitmapDrawable?)?.bitmap?.recycle()
|
||||
(mObtainedDrawable as? Animatable)?.run {
|
||||
stop()
|
||||
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
|
||||
}
|
||||
(mObtainedDrawable as? BitmapDrawable)?.run { bitmap.recycle() }
|
||||
mObtainedDrawable?.callback = null
|
||||
mObtainedDrawable = null
|
||||
release()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (!started) {
|
||||
(mObtainedDrawable as AnimatedImageDrawable?)?.start()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var screenWidth: Int = 0
|
||||
var screenHeight: Int = 0
|
||||
|
||||
init {
|
||||
val context = EhApplication.application
|
||||
screenWidth = context.resources.displayMetrics.widthPixels
|
||||
screenHeight = context.resources.displayMetrics.heightPixels
|
||||
}
|
||||
val screenWidth = EhApplication.application.resources.displayMetrics.widthPixels
|
||||
val screenHeight = EhApplication.application.resources.displayMetrics.heightPixels
|
||||
val isWideColorGamut = EhApplication.application.resources.configuration.isScreenWideColorGamut
|
||||
var colorSpace = ColorSpace.get(if (isWideColorGamut && EhApplication.readerPreferences.wideColorGamut().get()) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB)
|
||||
|
||||
@Throws(DecodeException::class)
|
||||
@JvmStatic
|
||||
@@ -100,11 +86,9 @@ class Image private constructor(source: Source?, val release: () -> Unit = {}) {
|
||||
|
||||
@Throws(DecodeException::class)
|
||||
@JvmStatic
|
||||
fun decode(buffer: ByteBuffer, release: () -> Unit? = {}): Image {
|
||||
fun decode(buffer: ByteBuffer): Image {
|
||||
val src = ImageDecoder.createSource(buffer)
|
||||
return Image(src) {
|
||||
release()
|
||||
}
|
||||
return Image(src, buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@ fun Context.getClipboardManager(): ClipboardManager {
|
||||
return getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
}
|
||||
|
||||
infix fun Context.tellClipboard(text: String?) {
|
||||
addTextToClipboard(text, false)
|
||||
}
|
||||
|
||||
fun Context.addTextToClipboard(text: String?, isSensitive: Boolean) {
|
||||
getClipboardManager().apply {
|
||||
setPrimaryClip(ClipData.newPlainText(null, text).apply {
|
||||
|
||||
@@ -39,4 +39,23 @@ public class HashCodeUtils {
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
public static String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -104,8 +104,6 @@ public class ContentLayout extends FrameLayout {
|
||||
mBottomProgress = mContentView.findViewById(R.id.bottom_progress);
|
||||
mFastScroller = mContentView.findViewById(R.id.fast_scroller);
|
||||
mRecyclerView = mRefreshLayout.findViewById(R.id.recycler_view);
|
||||
|
||||
mRefreshLayout.setColorSchemeColors(ResourcesKt.resolveColor(context.getTheme(), androidx.appcompat.R.attr.colorPrimary));
|
||||
mFastScroller.attachToRecyclerView(mRecyclerView);
|
||||
HandlerDrawable drawable = new HandlerDrawable();
|
||||
drawable.setColor(ResourcesKt.resolveColor(context.getTheme(), androidx.appcompat.R.attr.colorPrimary));
|
||||
@@ -192,6 +190,8 @@ public class ContentLayout extends FrameLayout {
|
||||
private static final String KEY_START_PAGE = "start_page";
|
||||
private static final String KEY_END_PAGE = "end_page";
|
||||
private static final String KEY_PAGES = "pages";
|
||||
private static final String KEY_PREV = "prev";
|
||||
private static final String KEY_NEXT = "next";
|
||||
/**
|
||||
* Generate task id
|
||||
*/
|
||||
@@ -227,17 +227,19 @@ public class ContentLayout extends FrameLayout {
|
||||
*/
|
||||
private int mPages;
|
||||
private int mNextPage;
|
||||
protected String mPrev;
|
||||
protected String mNext;
|
||||
private int mCurrentTaskId;
|
||||
private int mCurrentTaskType;
|
||||
private int mCurrentTaskPage;
|
||||
private final SwipeRefreshLayout.OnRefreshListener mOnRefreshListener = new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (mStartPage > 0) {
|
||||
if (mPrev != null || mStartPage > 0) {
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS;
|
||||
mCurrentTaskPage = mStartPage - 1;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, mPrev, false);
|
||||
} else {
|
||||
doRefresh();
|
||||
}
|
||||
@@ -248,31 +250,28 @@ public class ContentLayout extends FrameLayout {
|
||||
private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (!mRefreshLayout.isRefreshing() && !recyclerView.canScrollVertically(1) && mEndPage < mPages) {
|
||||
// Get next page
|
||||
mBottomProgress.show();
|
||||
if (mEndPage < mPages) {
|
||||
if (!mRefreshLayout.isRefreshing() && !recyclerView.canScrollVertically(1)) {
|
||||
if (mNext != null || mEndPage < mPages) {
|
||||
mBottomProgress.show();
|
||||
// Get next page
|
||||
// Fill pages before NextPage with empty list
|
||||
while (mNextPage > mEndPage && mEndPage < mPages) {
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS;
|
||||
mCurrentTaskPage = mEndPage;
|
||||
onGetPageData(mCurrentTaskId, mPages, mNextPage, Collections.emptyList());
|
||||
onGetPageData(mCurrentTaskId, mPages, mNextPage, null, null, Collections.emptyList());
|
||||
}
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS;
|
||||
mCurrentTaskPage = mEndPage;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
} else if (mEndPage == mPages) {
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, mNext, true);
|
||||
} else if (mStartPage > 0 && mEndPage == mPages) {
|
||||
mBottomProgress.show();
|
||||
// Refresh last page
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_REFRESH_PAGE;
|
||||
mCurrentTaskPage = mEndPage - 1;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
} else {
|
||||
Log.e(TAG, "Try to footer refresh, but mEndPage = " + mEndPage + ", mPages = " + mPages);
|
||||
mBottomProgress.hide();
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,6 +290,7 @@ public class ContentLayout extends FrameLayout {
|
||||
mRecyclerView = contentLayout.mRecyclerView;
|
||||
|
||||
Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.big_sad_pandroid);
|
||||
assert drawable != null;
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
mTipView.setCompoundDrawables(null, drawable, null, null);
|
||||
|
||||
@@ -304,19 +304,19 @@ public class ContentLayout extends FrameLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #onGetPageData(int, int, int, List)} when get data
|
||||
* Call {@link #onGetPageData(int, int, int, String, String, List)} when get data
|
||||
*
|
||||
* @param taskId task id
|
||||
* @param page the page to get
|
||||
* @param index the index to get
|
||||
* @param isNext the index is next or prev
|
||||
*/
|
||||
protected abstract void getPageData(int taskId, int type, int page);
|
||||
protected abstract void getPageData(int taskId, int type, int page, String index, boolean isNext);
|
||||
|
||||
protected abstract Context getContext();
|
||||
|
||||
protected abstract void notifyDataSetChanged();
|
||||
|
||||
protected abstract void notifyItemRangeChanged(int positionStart, int itemCount);
|
||||
|
||||
protected abstract void notifyItemRangeInserted(int positionStart, int itemCount);
|
||||
|
||||
protected void onScrollToPosition(int position) {
|
||||
@@ -355,6 +355,11 @@ public class ContentLayout extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public E getFirstVisibleItem() {
|
||||
return getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(mRecyclerView.getLayoutManager()));
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mData.size();
|
||||
}
|
||||
@@ -383,22 +388,16 @@ public class ContentLayout extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
protected void onAddData(E data) {
|
||||
}
|
||||
|
||||
protected void onAddData(List<E> data) {
|
||||
}
|
||||
|
||||
protected void onRemoveData(E data) {
|
||||
}
|
||||
|
||||
protected void onRemoveData(List<E> data) {
|
||||
}
|
||||
|
||||
protected void onClearData() {
|
||||
}
|
||||
|
||||
public void onGetPageData(int taskId, int pages, int nextPage, List<E> data) {
|
||||
public void onGetPageData(int taskId, int pages, int nextPage, String prev, String next, List<E> data) {
|
||||
if (mCurrentTaskId == taskId) {
|
||||
int dataSize;
|
||||
|
||||
@@ -408,6 +407,8 @@ public class ContentLayout extends FrameLayout {
|
||||
mEndPage = 1;
|
||||
mPages = pages;
|
||||
mNextPage = nextPage;
|
||||
mPrev = prev;
|
||||
mNext = next;
|
||||
mPageDivider.clear();
|
||||
mPageDivider.add(data.size());
|
||||
if (data.isEmpty()) {
|
||||
@@ -449,6 +450,7 @@ public class ContentLayout extends FrameLayout {
|
||||
mPageDivider.add(0, dataSize);
|
||||
mStartPage--;
|
||||
mPages = Math.max(mEndPage, pages);
|
||||
mPrev = prev;
|
||||
// assert mStartPage >= 0
|
||||
|
||||
if (data.isEmpty()) {
|
||||
@@ -475,7 +477,6 @@ public class ContentLayout extends FrameLayout {
|
||||
mData.addAll(0, data);
|
||||
onAddData(data);
|
||||
notifyItemRangeInserted(0, data.size());
|
||||
notifyItemRangeChanged(data.size(), mData.size() - data.size());
|
||||
|
||||
// Ui change, show content
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
@@ -504,6 +505,7 @@ public class ContentLayout extends FrameLayout {
|
||||
mEndPage++;
|
||||
mNextPage = nextPage;
|
||||
mPages = Math.max(mEndPage, pages);
|
||||
mNext = next;
|
||||
if (data.isEmpty()) {
|
||||
// OK, that's all
|
||||
if (mData.isEmpty()) {
|
||||
@@ -551,6 +553,8 @@ public class ContentLayout extends FrameLayout {
|
||||
mEndPage = mCurrentTaskPage + 1;
|
||||
mNextPage = nextPage;
|
||||
mPages = pages;
|
||||
mPrev = prev;
|
||||
mNext = next;
|
||||
mPageDivider.clear();
|
||||
mPageDivider.add(data.size());
|
||||
if (data.isEmpty()) {
|
||||
@@ -654,10 +658,6 @@ public class ContentLayout extends FrameLayout {
|
||||
mViewTransition.showView(0);
|
||||
}
|
||||
|
||||
protected void beforeRefresh() {
|
||||
|
||||
}
|
||||
|
||||
private boolean isContentShowing() {
|
||||
return mViewTransition.getShownViewIndex() == 0;
|
||||
}
|
||||
@@ -679,12 +679,11 @@ public class ContentLayout extends FrameLayout {
|
||||
showText(mEmptyString);
|
||||
}
|
||||
|
||||
protected void doRefresh() {
|
||||
beforeRefresh();
|
||||
private void doRefresh() {
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_REFRESH;
|
||||
mCurrentTaskPage = 0;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -757,42 +756,44 @@ public class ContentLayout extends FrameLayout {
|
||||
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), position, 0);
|
||||
onScrollToPosition(position);
|
||||
} else if (page == mStartPage - 1) {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
mRefreshLayout.setRefreshing(true);
|
||||
mBottomProgress.hide();
|
||||
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_PRE_PAGE;
|
||||
mCurrentTaskPage = page;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true);
|
||||
} else if (page == mEndPage) {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
mBottomProgress.hide();
|
||||
mBottomProgress.show();
|
||||
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE;
|
||||
mCurrentTaskPage = page;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true);
|
||||
} else {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
mRefreshLayout.setRefreshing(true);
|
||||
mBottomProgress.hide();
|
||||
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_SOMEWHERE;
|
||||
mCurrentTaskPage = page;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doGoToPage(int page) {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
public void goTo(String index, boolean isNext) {
|
||||
mRefreshLayout.setRefreshing(true);
|
||||
mBottomProgress.hide();
|
||||
mCurrentTaskId = mIdGenerator.nextId();
|
||||
mCurrentTaskType = TYPE_SOMEWHERE;
|
||||
mCurrentTaskPage = page;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
|
||||
mCurrentTaskPage = 0;
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, index, isNext);
|
||||
}
|
||||
|
||||
protected Parcelable saveInstanceState(Parcelable superState) {
|
||||
cancelCurrentTask();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(KEY_SUPER, superState);
|
||||
int shownView = mViewTransition.getShownViewIndex();
|
||||
@@ -813,6 +814,8 @@ public class ContentLayout extends FrameLayout {
|
||||
bundle.putInt(KEY_START_PAGE, mStartPage);
|
||||
bundle.putInt(KEY_END_PAGE, mEndPage);
|
||||
bundle.putInt(KEY_PAGES, mPages);
|
||||
bundle.putString(KEY_PREV, mPrev);
|
||||
bundle.putString(KEY_NEXT, mNext);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@@ -837,6 +840,8 @@ public class ContentLayout extends FrameLayout {
|
||||
mStartPage = bundle.getInt(KEY_START_PAGE);
|
||||
mEndPage = bundle.getInt(KEY_END_PAGE);
|
||||
mPages = bundle.getInt(KEY_PAGES);
|
||||
mPrev = bundle.getString(KEY_PREV);
|
||||
mNext = bundle.getString(KEY_NEXT);
|
||||
|
||||
notifyDataSetChanged();
|
||||
|
||||
@@ -845,6 +850,8 @@ public class ContentLayout extends FrameLayout {
|
||||
mStartPage = 0;
|
||||
mEndPage = 0;
|
||||
mPages = 0;
|
||||
mPrev = null;
|
||||
mNext = null;
|
||||
firstRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
open class Page(val index: Int) {
|
||||
|
||||
@@ -9,7 +10,14 @@ open class Page(val index: Int) {
|
||||
|
||||
var status = MutableStateFlow(State.QUEUE)
|
||||
|
||||
var progress = MutableStateFlow(0f)
|
||||
private val _progressFlow = MutableStateFlow(0)
|
||||
|
||||
val progressFlow = _progressFlow.asStateFlow()
|
||||
var progress: Int
|
||||
get() = _progressFlow.value
|
||||
set(value) {
|
||||
_progressFlow.value = value
|
||||
}
|
||||
|
||||
enum class State {
|
||||
QUEUE,
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorMatrix
|
||||
@@ -68,20 +69,18 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.databinding.ReaderActivityBinding
|
||||
import com.hippo.ehviewer.gallery.ArchivePageLoader
|
||||
import com.hippo.ehviewer.gallery.DirPageLoader
|
||||
import com.hippo.ehviewer.gallery.EhPageLoader
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
@@ -161,7 +160,7 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
private var mGalleryProvider: PageLoader2? = null
|
||||
var mGalleryProvider: PageLoader2? = null
|
||||
private var mSize: Int = 0
|
||||
private var mCurrentIndex: Int = 0
|
||||
private var mSavingPage = -1
|
||||
@@ -197,16 +196,10 @@ class ReaderActivity : EhActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
if (ACTION_DIR == mAction) {
|
||||
if (mFilename != null) {
|
||||
mGalleryProvider = DirPageLoader(
|
||||
UniFile.fromFile(File(mFilename!!))!!
|
||||
)
|
||||
}
|
||||
} else if (ACTION_EH == mAction) {
|
||||
if (ACTION_EH == mAction) {
|
||||
if (mGalleryInfo != null) {
|
||||
mGalleryProvider =
|
||||
EhPageLoader(this, mGalleryInfo)
|
||||
EhPageLoader(mGalleryInfo)
|
||||
}
|
||||
} else if (Intent.ACTION_VIEW == mAction) {
|
||||
if (mUri != null) {
|
||||
@@ -260,6 +253,7 @@ class ReaderActivity : EhActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window.colorMode = if (Image.isWideColorGamut && readerPreferences.wideColorGamut().get()) ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT else ActivityInfo.COLOR_MODE_DEFAULT
|
||||
if (savedInstanceState == null) {
|
||||
onInit()
|
||||
} else {
|
||||
@@ -278,13 +272,12 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
ArchivePageLoader.showPasswd = ShowPasswdDialogHandler(this)
|
||||
|
||||
mGalleryProvider!!.start()
|
||||
|
||||
// Get start page
|
||||
val startPage: Int = if (savedInstanceState == null) {
|
||||
if (mPage >= 0) mPage else mGalleryProvider!!.startPage
|
||||
} else {
|
||||
mCurrentIndex
|
||||
if (savedInstanceState == null) {
|
||||
mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage
|
||||
}
|
||||
mCurrentIndex = startPage
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mGalleryProvider!!.state.collect {
|
||||
@@ -295,8 +288,6 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
mGalleryProvider!!.start()
|
||||
|
||||
config = ReaderConfig()
|
||||
initializeMenu()
|
||||
}
|
||||
@@ -308,6 +299,7 @@ class ReaderActivity : EhActivity() {
|
||||
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
||||
viewer?.destroy()
|
||||
viewer = ReadingModeType.toViewer(readerPreferences.defaultReadingMode().get(), this)
|
||||
binding.pageSlider.isRTL = viewer is R2LPagerViewer
|
||||
updateViewerInset(readerPreferences.fullscreen().get())
|
||||
binding.viewerContainer.removeAllViews()
|
||||
setOrientation(readerPreferences.defaultOrientationType().get())
|
||||
@@ -424,7 +416,7 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage(page: Int) {
|
||||
fun saveImage(page: Int) {
|
||||
if (null == mGalleryProvider) {
|
||||
return
|
||||
}
|
||||
@@ -592,7 +584,6 @@ class ReaderActivity : EhActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_DIR = "dir"
|
||||
const val ACTION_EH = "eh"
|
||||
const val KEY_ACTION = "action"
|
||||
const val KEY_FILENAME = "filename"
|
||||
@@ -601,7 +592,7 @@ class ReaderActivity : EhActivity() {
|
||||
const val KEY_PAGE = "page"
|
||||
const val KEY_CURRENT_INDEX = "current_index"
|
||||
|
||||
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(EhApplication.application)) }
|
||||
val readerPreferences = EhApplication.readerPreferences
|
||||
}
|
||||
|
||||
/* Tachiyomi funcs */
|
||||
@@ -637,18 +628,6 @@ class ReaderActivity : EhActivity() {
|
||||
binding.readerMenu.isVisible = true
|
||||
|
||||
if (animate) {
|
||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
|
||||
toolbarAnimation.applySystemAnimatorScale(this)
|
||||
toolbarAnimation.setAnimationListener(
|
||||
object : SimpleAnimationListener() {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
// Fix status bar being translucent the first time it's opened.
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.toolbar.startAnimation(toolbarAnimation)
|
||||
|
||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
||||
bottomAnimation.applySystemAnimatorScale(this)
|
||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||
@@ -660,23 +639,20 @@ class ReaderActivity : EhActivity() {
|
||||
} else {
|
||||
if (readerPreferences.fullscreen().get()) {
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
|
||||
toolbarAnimation.applySystemAnimatorScale(this)
|
||||
toolbarAnimation.setAnimationListener(
|
||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
|
||||
bottomAnimation.applySystemAnimatorScale(this)
|
||||
bottomAnimation.setAnimationListener(
|
||||
object : SimpleAnimationListener() {
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.readerMenu.isVisible = false
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.toolbar.startAnimation(toolbarAnimation)
|
||||
|
||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
|
||||
bottomAnimation.applySystemAnimatorScale(this)
|
||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||
}
|
||||
|
||||
@@ -691,17 +667,6 @@ class ReaderActivity : EhActivity() {
|
||||
*/
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun initializeMenu() {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
binding.toolbar.applyInsetter {
|
||||
type(navigationBars = true, statusBars = true) {
|
||||
margin(top = true, horizontal = true)
|
||||
}
|
||||
}
|
||||
binding.readerMenuBottom.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
margin(bottom = true, horizontal = true)
|
||||
@@ -730,10 +695,10 @@ class ReaderActivity : EhActivity() {
|
||||
|
||||
initBottomShortcuts()
|
||||
|
||||
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
|
||||
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this).apply {
|
||||
elevation =
|
||||
resources.getDimension(com.google.android.material.R.dimen.m3_sys_elevation_level2)
|
||||
alpha = 0
|
||||
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
|
||||
}
|
||||
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
|
||||
|
||||
@@ -778,7 +743,8 @@ class ReaderActivity : EhActivity() {
|
||||
setTooltip(R.string.pref_crop_borders)
|
||||
|
||||
setOnClickListener {
|
||||
val isPagerType = ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get())
|
||||
val isPagerType =
|
||||
ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get())
|
||||
if (isPagerType) {
|
||||
readerPreferences.cropBorders().toggle()
|
||||
} else {
|
||||
@@ -816,7 +782,7 @@ class ReaderActivity : EhActivity() {
|
||||
setTooltip(R.string.action_settings)
|
||||
val readerSettingSheetDialog = ReaderSettingsSheet(this@ReaderActivity)
|
||||
setOnClickListener {
|
||||
if (!readerSettingSheetDialog.isShowing()) {
|
||||
if (!readerSettingSheetDialog.isShowing) {
|
||||
readerSettingSheetDialog.show()
|
||||
}
|
||||
}
|
||||
@@ -879,7 +845,7 @@ class ReaderActivity : EhActivity() {
|
||||
* Moves the viewer to the given page [index]. It does nothing if the viewer is null or the
|
||||
* page is not found.
|
||||
*/
|
||||
fun moveToPageIndex(index: Int) {
|
||||
private fun moveToPageIndex(index: Int) {
|
||||
val viewer = viewer ?: return
|
||||
val page = mGalleryProvider?.mPages?.getOrNull(index) ?: return
|
||||
viewer.moveToPage(page)
|
||||
@@ -910,7 +876,8 @@ class ReaderActivity : EhActivity() {
|
||||
binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f)
|
||||
binding.pageSlider.value = page.index.toFloat()
|
||||
|
||||
mGalleryProvider?.putStartPage(page.index)
|
||||
mCurrentIndex = page.index
|
||||
mGalleryProvider?.putStartPage(mCurrentIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ class ReaderPageSheet(
|
||||
|
||||
binding.share.setOnClickListener { share() }
|
||||
binding.save.setOnClickListener { save() }
|
||||
binding.saveTo.setOnClickListener { saveTo() }
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@@ -37,6 +38,14 @@ class ReaderPageSheet(
|
||||
* Saves the image of this page on external storage.
|
||||
*/
|
||||
private fun save() {
|
||||
activity.saveImage(page.index)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the image of this page to a custom location on external storage.
|
||||
*/
|
||||
private fun saveTo() {
|
||||
activity.saveImageTo(page.index)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.collection.LruCache
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import com.hippo.yorozuya.OSUtils
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
abstract class PageLoader {
|
||||
|
||||
private val mImageCache = ImageCache()
|
||||
val mPages by lazy {
|
||||
check(size() > 0)
|
||||
(0 until size()).map { ReaderPage(it) }
|
||||
@@ -17,12 +23,28 @@ abstract class PageLoader {
|
||||
|
||||
abstract fun start()
|
||||
|
||||
abstract fun stop()
|
||||
@CallSuper
|
||||
open fun stop() {
|
||||
mImageCache.evictAll()
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
mImageCache.evictAll()
|
||||
}
|
||||
|
||||
abstract fun size(): Int
|
||||
|
||||
fun request(index: Int) {
|
||||
onRequest(index)
|
||||
fun request(page: ReaderPage) {
|
||||
val index = mPages.indexOf(page)
|
||||
val image = mImageCache[index]
|
||||
if (image != null)
|
||||
notifyPageSucceed(index, image)
|
||||
else
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
fun retryPage(page: ReaderPage) {
|
||||
onForceRequest(mPages.indexOf(page))
|
||||
}
|
||||
|
||||
protected abstract fun onRequest(index: Int)
|
||||
@@ -42,30 +64,45 @@ abstract class PageLoader {
|
||||
state.compareAndSet(STATE_WAIT, STATE_READY)
|
||||
}
|
||||
|
||||
fun notifyDataChanged(index: Int) {
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
fun notifyPageWait(index: Int) {
|
||||
mPages[index].status.value = Page.State.QUEUE
|
||||
}
|
||||
|
||||
fun notifyPagePercent(index: Int, percent: Float) {
|
||||
mPages[index].status.compareAndSet(Page.State.QUEUE, Page.State.DOWNLOAD_IMAGE)
|
||||
mPages[index].progress.value = percent
|
||||
mPages[index].progress = (percent * 100).toInt()
|
||||
}
|
||||
|
||||
fun notifyPageSucceed(index: Int, image: Image) {
|
||||
val priv = mPages[index].image
|
||||
if (mImageCache[index] != image)
|
||||
mImageCache.add(index, image)
|
||||
mPages[index].image = image
|
||||
mPages[index].status.value = Page.State.READY
|
||||
priv?.recycle()
|
||||
}
|
||||
|
||||
fun notifyPageFailed(index: Int) {
|
||||
mPages[index].status.value = Page.State.ERROR
|
||||
}
|
||||
|
||||
private class ImageCache : LruCache<Int, Image>(MathUtils.clamp(OSUtils.getTotalMemory() / 16, MIN_CACHE_SIZE, MAX_CACHE_SIZE).toInt()) {
|
||||
fun add(key: Int, value: Image) {
|
||||
put(key, value)
|
||||
}
|
||||
|
||||
override fun sizeOf(key: Int, value: Image): Int {
|
||||
return value.height * value.width * 4
|
||||
}
|
||||
|
||||
override fun entryRemoved(evicted: Boolean, key: Int, oldValue: Image, newValue: Image?) {
|
||||
oldValue.recycle()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CACHE_SIZE = (512 * 1024 * 1024).toLong()
|
||||
private const val MIN_CACHE_SIZE = (128 * 1024 * 1024).toLong()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STATE_WAIT = -1
|
||||
const val STATE_ERROR = -2
|
||||
|
||||
+22
-1
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.setting
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.ColorSpace
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.ColorInt
|
||||
@@ -12,7 +14,9 @@ import androidx.core.widget.NestedScrollView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
import com.hippo.ehviewer.databinding.ReaderColorFilterSettingsBinding
|
||||
import com.hippo.image.Image
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.preference.asHotFlow
|
||||
import eu.kanade.tachiyomi.util.preference.bindToPreference
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -31,9 +35,26 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
init {
|
||||
addView(binding.root)
|
||||
|
||||
binding.wideColorGamut.isEnabled = Image.isWideColorGamut
|
||||
if (!Image.isWideColorGamut)
|
||||
readerPreferences.wideColorGamut().set(false)
|
||||
binding.wideColorGamut.bindToPreference(readerPreferences.wideColorGamut())
|
||||
|
||||
readerPreferences.wideColorGamut().asHotFlow {
|
||||
Image.colorSpace = ColorSpace.get(if (Image.isWideColorGamut && it) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB)
|
||||
val colorMode = if (Image.isWideColorGamut && it) ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT else ActivityInfo.COLOR_MODE_DEFAULT
|
||||
(context as ReaderActivity).run {
|
||||
if (window.colorMode != colorMode) {
|
||||
window.colorMode = colorMode
|
||||
mGalleryProvider?.restart()
|
||||
setGallery()
|
||||
}
|
||||
}
|
||||
}.launchIn((context as ReaderActivity).lifecycleScope)
|
||||
|
||||
readerPreferences.colorFilter().changes()
|
||||
.onEach { setColorFilter(it) }
|
||||
.launchIn((context as ReaderActivity).lifecycleScope)
|
||||
.launchIn(context.lifecycleScope)
|
||||
|
||||
readerPreferences.colorFilterMode().changes()
|
||||
.onEach { setColorFilter(readerPreferences.colorFilter().get()) }
|
||||
|
||||
@@ -47,5 +47,10 @@ class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: A
|
||||
binding.keepscreen.bindToPreference(readerPreferences.keepScreenOn())
|
||||
binding.longTap.bindToPreference(readerPreferences.readWithLongTap())
|
||||
binding.pageTransitions.bindToPreference(readerPreferences.pageTransitions())
|
||||
binding.volumePage.bindToPreference(readerPreferences.readWithVolumeKeys())
|
||||
binding.reserveVolumePage.bindToPreference(readerPreferences.readWithVolumeKeysInverted())
|
||||
readerPreferences.readWithVolumeKeys().asHotFlow {
|
||||
binding.reserveVolumePage.isVisible = it
|
||||
}.launchIn((context as ReaderActivity).lifecycleScope)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class ReaderPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun navigateToPan() = preferenceStore.getBoolean("navigate_pan", true)
|
||||
|
||||
fun landscapeZoom() = preferenceStore.getBoolean("landscape_zoom", true)
|
||||
fun landscapeZoom() = preferenceStore.getBoolean("landscape_zoom", false)
|
||||
|
||||
fun cropBordersWebtoon() = preferenceStore.getBoolean("crop_borders_webtoon", false)
|
||||
|
||||
@@ -106,5 +106,7 @@ class ReaderPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun showNavigationOverlayOnStart() = preferenceStore.getBoolean("reader_navigation_overlay_on_start", false)
|
||||
|
||||
fun wideColorGamut() = preferenceStore.getBoolean("wide_color_gamut", false)
|
||||
|
||||
// endregion
|
||||
}
|
||||
+34
-8
@@ -7,8 +7,8 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hippo.ehviewer.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import com.hippo.ehviewer.databinding.ReaderReadingModeSettingsBinding
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||
@@ -19,12 +19,16 @@ import kotlinx.coroutines.flow.launchIn
|
||||
/**
|
||||
* Sheet to show reader and viewer preferences.
|
||||
*/
|
||||
class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
class ReaderReadingModeSettings @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) :
|
||||
NestedScrollView(context, attrs) {
|
||||
|
||||
private val readerPreferences: ReaderPreferences = ReaderActivity.readerPreferences
|
||||
|
||||
private val binding = ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false)
|
||||
private val binding =
|
||||
ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false)
|
||||
|
||||
init {
|
||||
addView(binding.root)
|
||||
@@ -53,14 +57,18 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||
initPagerPreferences()
|
||||
}
|
||||
}
|
||||
binding.viewer.setSelection(ReaderActivity.readerPreferences.defaultReadingMode().get().let { ReadingModeType.fromPreference(it).prefValue })
|
||||
binding.viewer.setSelection(
|
||||
ReaderActivity.readerPreferences.defaultReadingMode().get()
|
||||
.let { ReadingModeType.fromPreference(it).prefValue })
|
||||
|
||||
binding.rotationMode.onItemSelectedListener = { position ->
|
||||
val rotationType = OrientationType.fromSpinner(position)
|
||||
readerPreferences.defaultOrientationType().set(rotationType.flagValue)
|
||||
(context as ReaderActivity).setGallery()
|
||||
}
|
||||
binding.rotationMode.setSelection(ReaderActivity.readerPreferences.defaultOrientationType().get().let { OrientationType.fromPreference(it).prefValue })
|
||||
binding.rotationMode.setSelection(
|
||||
ReaderActivity.readerPreferences.defaultOrientationType().get()
|
||||
.let { OrientationType.fromPreference(it).prefValue })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +78,10 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||
binding.webtoonPrefsGroup.root.isVisible = false
|
||||
binding.pagerPrefsGroup.root.isVisible = true
|
||||
|
||||
binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), PreferenceValues.TappingInvertMode::class.java)
|
||||
binding.pagerPrefsGroup.tappingInverted.bindToPreference(
|
||||
readerPreferences.pagerNavInverted(),
|
||||
PreferenceValues.TappingInvertMode::class.java
|
||||
)
|
||||
binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan())
|
||||
|
||||
binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager())
|
||||
@@ -99,13 +110,28 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
|
||||
binding.pagerPrefsGroup.root.isVisible = false
|
||||
binding.webtoonPrefsGroup.root.isVisible = true
|
||||
|
||||
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), PreferenceValues.TappingInvertMode::class.java)
|
||||
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(
|
||||
readerPreferences.webtoonNavInverted(),
|
||||
PreferenceValues.TappingInvertMode::class.java
|
||||
)
|
||||
|
||||
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon())
|
||||
readerPreferences.navigationModeWebtoon()
|
||||
.asHotFlow { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 }
|
||||
.launchIn((context as ReaderActivity).lifecycleScope)
|
||||
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon())
|
||||
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
||||
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(
|
||||
readerPreferences.webtoonSidePadding(),
|
||||
R.array.webtoon_side_padding_values
|
||||
)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
initGeneralPreferences()
|
||||
|
||||
when ((context as ReaderActivity).viewer) {
|
||||
is PagerViewer -> initPagerPreferences()
|
||||
is WebtoonViewer -> initWebtoonPreferences()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.setting
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.hippo.ehviewer.R
|
||||
@@ -32,7 +33,11 @@ class ReaderSettingsSheet(
|
||||
ValueAnimator.ofFloat(sheetBackgroundDim, 0f).also { valueAnimator ->
|
||||
valueAnimator.duration = 250
|
||||
valueAnimator.addUpdateListener {
|
||||
window?.setDimAmount(it.animatedValue as Float)
|
||||
window?.run {
|
||||
setDimAmount(it.animatedValue as Float)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
attributes.blurBehindRadius = (it.animatedValue as Float * 50).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +91,7 @@ class ReaderSettingsSheet(
|
||||
)
|
||||
|
||||
override fun show() {
|
||||
initSettings()
|
||||
readingModeSettings.init()
|
||||
super.show()
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.E
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.SCALE_TYPE_CENTER_INSIDE
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
||||
import java.io.InputStream
|
||||
@@ -220,7 +219,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
} else {
|
||||
SubsamplingScaleImageView(context)
|
||||
}.apply {
|
||||
setMaxTileSize(GLUtil.maxTextureSize)
|
||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||
setMinimumTileDpi(180)
|
||||
@@ -278,7 +276,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
||||
when (image) {
|
||||
is Drawable -> {
|
||||
val bitmap = (image as BitmapDrawable).bitmap
|
||||
setImage(ImageSource.bitmap(bitmap))
|
||||
setImage(ImageSource.cachedBitmap(bitmap))
|
||||
}
|
||||
is InputStream -> setImage(ImageSource.inputStream(image))
|
||||
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
|
||||
|
||||
@@ -11,10 +11,9 @@ import androidx.annotation.IntRange
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
|
||||
/**
|
||||
* A wrapper for [CircularProgressIndicator] that always rotates while being determinate.
|
||||
* A wrapper for [CircularProgressIndicator] that always rotates.
|
||||
*
|
||||
* By always rotating we give the feedback to the user that the application isn't 'stuck',
|
||||
* and by making it determinate the user also approximately knows how much the operation will take.
|
||||
* By always rotating we give the feedback to the user that the application isn't 'stuck'.
|
||||
*/
|
||||
class ReaderProgressIndicator @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
||||
@@ -39,19 +39,18 @@ class PagerPageHolder(
|
||||
/**
|
||||
* Loading progress bar to indicate the current progress.
|
||||
*/
|
||||
private val progressIndicator: ReaderProgressIndicator =
|
||||
ReaderProgressIndicator(readerThemedContext).apply {
|
||||
updateLayoutParams<LayoutParams> {
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
|
||||
updateLayoutParams<LayoutParams> {
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error layout to show when the image fails to load.
|
||||
*/
|
||||
private var errorLayout: ReaderErrorBinding? = null
|
||||
|
||||
private var scope = CoroutineScope(Dispatchers.IO)
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
@@ -59,17 +58,12 @@ class PagerPageHolder(
|
||||
private var statusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription for progress changes of the page.
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var progressJob: Job? = null
|
||||
|
||||
init {
|
||||
addView(progressIndicator)
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progress.collect {
|
||||
progressIndicator.setProgress(it.toInt())
|
||||
}
|
||||
}
|
||||
statusJob = scope.launch(Dispatchers.Main) {
|
||||
page.status.collectLatest {
|
||||
processStatus(it)
|
||||
@@ -83,10 +77,17 @@ class PagerPageHolder(
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unsubscribeProgress()
|
||||
cancelProgressJob()
|
||||
unsubscribeStatus()
|
||||
}
|
||||
|
||||
private fun launchProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the status of the page changes.
|
||||
*
|
||||
@@ -97,17 +98,16 @@ class PagerPageHolder(
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
}
|
||||
|
||||
Page.State.READY -> {
|
||||
page.image?.mObtainedDrawable?.let { setImage(it) }
|
||||
unsubscribeProgress()
|
||||
cancelProgressJob()
|
||||
}
|
||||
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress()
|
||||
cancelProgressJob()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,13 +117,12 @@ class PagerPageHolder(
|
||||
*/
|
||||
private fun unsubscribeStatus() {
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun unsubscribeProgress() {
|
||||
private fun cancelProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +202,7 @@ class PagerPageHolder(
|
||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
errorLayout?.actionRetry?.viewer = viewer
|
||||
errorLayout?.actionRetry?.setOnClickListener {
|
||||
TODO()
|
||||
viewer.activity.mGalleryProvider?.retryPage(page)
|
||||
}
|
||||
}
|
||||
errorLayout?.root?.isVisible = true
|
||||
|
||||
@@ -54,13 +54,17 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
* Creates a new view for the item at the given [position].
|
||||
*/
|
||||
override fun createView(container: ViewGroup, position: Int): View {
|
||||
currentChapter?.request(position)
|
||||
currentChapter?.request(items[position] as ReaderPage)
|
||||
return when (val item = items[position]) {
|
||||
is ReaderPage -> PagerPageHolder(readerThemedContext, viewer, item)
|
||||
else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroyView(container: ViewGroup, position: Int, view: View) {
|
||||
currentChapter?.cancelRequest(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the given [view] on the adapter.
|
||||
*/
|
||||
|
||||
@@ -74,9 +74,9 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
*/
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
currentChapter!!.request(position)
|
||||
currentChapter!!.request(item as ReaderPage)
|
||||
when (holder) {
|
||||
is WebtoonPageHolder -> holder.bind(item as ReaderPage)
|
||||
is WebtoonPageHolder -> holder.bind(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
* Recycles an existing view [holder] before adding it to the view pool.
|
||||
*/
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
currentChapter?.cancelRequest(holder.bindingAdapterPosition)
|
||||
when (holder) {
|
||||
is WebtoonPageHolder -> holder.recycle()
|
||||
}
|
||||
|
||||
+36
-14
@@ -63,14 +63,21 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private var page: ReaderPage? = null
|
||||
|
||||
private var progressJob: Job? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
*/
|
||||
private var statusJob: Job? = null
|
||||
|
||||
private var scope = CoroutineScope(Dispatchers.IO)
|
||||
/**
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var progressJob: Job? = null
|
||||
|
||||
init {
|
||||
refreshLayoutParams()
|
||||
|
||||
frame.onImageLoaded = { onImageDecoded() }
|
||||
frame.onImageLoadError = { onImageDecodeError() }
|
||||
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
||||
@@ -81,11 +88,6 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
fun bind(page: ReaderPage) {
|
||||
this.page = page
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progress.collect {
|
||||
progressIndicator.setProgress(it.toInt())
|
||||
}
|
||||
}
|
||||
statusJob = scope.launch(Dispatchers.Main) {
|
||||
page.status.collectLatest {
|
||||
processStatus(it)
|
||||
@@ -111,13 +113,26 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
override fun recycle() {
|
||||
statusJob?.cancel()
|
||||
progressJob?.cancel()
|
||||
cancelProgressJob()
|
||||
|
||||
removeErrorLayout()
|
||||
frame.recycle()
|
||||
page?.image?.recycle()
|
||||
progressIndicator.setProgress(0, animated = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the progress of the page and updates view.
|
||||
*/
|
||||
private fun launchProgressJob() {
|
||||
cancelProgressJob()
|
||||
|
||||
val page = page ?: return
|
||||
|
||||
progressJob = scope.launch(Dispatchers.Main) {
|
||||
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the status of the page changes.
|
||||
*
|
||||
@@ -128,21 +143,28 @@ class WebtoonPageHolder(
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
}
|
||||
|
||||
Page.State.READY -> {
|
||||
page?.image?.mObtainedDrawable?.let { setImage(it) }
|
||||
progressJob?.cancel()
|
||||
cancelProgressJob()
|
||||
}
|
||||
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
progressJob?.cancel()
|
||||
cancelProgressJob()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun cancelProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page is queued.
|
||||
*/
|
||||
@@ -232,7 +254,7 @@ class WebtoonPageHolder(
|
||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
||||
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
||||
errorLayout?.actionRetry?.setOnClickListener {
|
||||
TODO()
|
||||
viewer.activity.mGalleryProvider?.retryPage(page!!)
|
||||
}
|
||||
}
|
||||
return errorLayout!!
|
||||
|
||||
@@ -12,16 +12,14 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.WebtoonLayoutManager
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Implementation of a [BaseViewer] to display pages with a [RecyclerView].
|
||||
@@ -171,9 +169,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
val position = adapter.items.indexOf(page)
|
||||
if (position != -1) {
|
||||
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
|
||||
onScrolled(pos = position)
|
||||
}
|
||||
onScrolled(pos = position)
|
||||
} else {
|
||||
logcat { "Page $page not found in adapter" }
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10
|
||||
import javax.microedition.khronos.egl.EGLConfig
|
||||
import javax.microedition.khronos.egl.EGLContext
|
||||
import kotlin.math.max
|
||||
|
||||
object GLUtil {
|
||||
val maxTextureSize: Int by lazy {
|
||||
// Get EGL Display
|
||||
val egl = EGLContext.getEGL() as EGL10
|
||||
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
|
||||
|
||||
// Initialise
|
||||
val version = IntArray(2)
|
||||
egl.eglInitialize(display, version)
|
||||
|
||||
// Query total number of configurations
|
||||
val totalConfigurations = IntArray(1)
|
||||
egl.eglGetConfigs(display, null, 0, totalConfigurations)
|
||||
|
||||
// Query actual list configurations
|
||||
val configurationsList = arrayOfNulls<EGLConfig>(totalConfigurations[0])
|
||||
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations)
|
||||
|
||||
val textureSize = IntArray(1)
|
||||
var maximumTextureSize = 0
|
||||
|
||||
// Iterate through all the configurations to located the maximum texture size
|
||||
for (i in 0 until totalConfigurations[0]) {
|
||||
// Only need to check for width since opengl textures are always squared
|
||||
egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize)
|
||||
|
||||
// Keep track of the maximum texture size
|
||||
if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0]
|
||||
}
|
||||
|
||||
// Release
|
||||
egl.eglTerminate(display)
|
||||
|
||||
// Return largest texture size found, or default
|
||||
max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION)
|
||||
}
|
||||
}
|
||||
|
||||
// Safe minimum default size
|
||||
private const val IMAGE_MAX_BITMAP_DIMENSION = 2048
|
||||
@@ -73,8 +73,10 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
}
|
||||
selectedPosition = selection
|
||||
popup?.menu?.getItem(selectedPosition)?.let {
|
||||
it.icon = checkmarkIcon
|
||||
if (selectedPosition < (popup?.menu?.size ?: 0)) {
|
||||
popup?.menu?.getItem(selectedPosition)?.let {
|
||||
it.icon = checkmarkIcon
|
||||
}
|
||||
}
|
||||
binding.details.text = entries.getOrNull(selection).orEmpty()
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate
|
||||
android:duration="200"
|
||||
android:fromYDelta="-100%"
|
||||
android:toYDelta="0%" />
|
||||
</set>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate
|
||||
android:duration="200"
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="-100%" />
|
||||
</set>
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2022 Tarsin Norbin
|
||||
~
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:color="?colorControlHighlight"
|
||||
tools:ignore="PrivateResource">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:right="8dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners
|
||||
android:bottomRightRadius="32dp"
|
||||
android:topRightRadius="32dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorPrimary"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_book_open" />
|
||||
|
||||
</vector>
|
||||
@@ -4,8 +4,8 @@
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU GeneralPublic License as published
|
||||
~ by the Free Software Foundation,either version 3 of the License, or
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2022 Tarsin Norbin
|
||||
~
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,18 L5.6,16.6 10.2,12 5.6,7.4 7,6 13,12ZM16,18V6H18V18Z" />
|
||||
</vector>
|
||||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user