Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7fcda1f83 | ||
|
|
b832c24185 | ||
|
|
3b04f8c695 | ||
|
|
0ef11b58d2 | ||
|
|
bf13bbf506 | ||
|
|
92c94aa22a | ||
|
|
1c0785432b | ||
|
|
dcb9a50061 | ||
|
|
de7f4bdb6e | ||
|
|
73d636dba6 | ||
|
|
3f1c453ff6 | ||
|
|
ecb6baaebb | ||
|
|
e1cba45486 | ||
|
|
ea4181b243 | ||
|
|
6c5b9f8851 | ||
|
|
c1fccaf240 | ||
|
|
e7c223da7d | ||
|
|
6bfe1ba876 | ||
|
|
574e5dab18 | ||
|
|
5dc9b83831 | ||
|
|
57a214a9a4 | ||
|
|
cf48eb94af | ||
|
|
683f1e488a | ||
|
|
e888ae4da7 | ||
|
|
4512f2a375 | ||
|
|
68442afa0f | ||
|
|
ddf4d0cbc3 | ||
|
|
4be185e428 | ||
|
|
f75e66ac19 | ||
|
|
abda781f02 | ||
|
|
c1533ec66d | ||
|
|
c8319446b0 | ||
|
|
7ae452cf13 | ||
|
|
4c024c5faa | ||
|
|
9d16e9d98c | ||
|
|
48357d08f7 | ||
|
|
8e0bcf40db | ||
|
|
85ef5f04dd | ||
|
|
18e0748d14 | ||
|
|
cdde5b098e | ||
|
|
f83964752d | ||
|
|
0c3e34bbf9 | ||
|
|
6dea7faac8 | ||
|
|
726f482c33 | ||
|
|
cb242f664a | ||
|
|
edc0345970 | ||
|
|
a50535dd76 | ||
|
|
505942aa25 | ||
|
|
38a593a9de | ||
|
|
da87fb2551 | ||
|
|
d4dc65a4ba | ||
|
|
53463caedd | ||
|
|
0707a0fad8 | ||
|
|
dd1024af27 | ||
|
|
7edc58a6da | ||
|
|
5a5da0cace | ||
|
|
53f02ed366 | ||
|
|
27cfa885e7 | ||
|
|
beb132d604 | ||
|
|
b1b10298b4 | ||
|
|
b1993ff7d2 | ||
|
|
2afe5fe5b2 | ||
|
|
52fa8f64cc | ||
|
|
4f7978070f | ||
|
|
a909983822 | ||
|
|
5267cad896 | ||
|
|
ce274cf65b | ||
|
|
55b6b2e34d | ||
|
|
5e4e2971b5 | ||
|
|
a933bcfb1e | ||
|
|
b4b38e27f3 | ||
|
|
bc70824b57 | ||
|
|
87ecaaa6a4 | ||
|
|
c4cd9cd802 | ||
|
|
bbe0e8c5a9 | ||
|
|
3f8984d2dc |
No files matched your search
@@ -73,6 +73,12 @@ with Material Design 3 and Dynamic Color Support
|
||||

|
||||

|
||||
|
||||
# Localization
|
||||
|
||||
Thank you to each of the **Ehviewer Overhauled** translators, and if you would like to contribute, please submit a translation via [Weblate](https://hosted.weblate.org/engage/ehviewer-overhauled/).
|
||||
|
||||
[<img src="https://hosted.weblate.org/widgets/ehviewer-overhauled/-/horizontal-auto.svg" alt="" />](https://hosted.weblate.org/engage/ehviewer-overhauled/)
|
||||
|
||||
# Thanks
|
||||
|
||||
Here is the libraries
|
||||
|
||||
+10
-6
@@ -32,9 +32,9 @@ android {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 180030
|
||||
versionName "1.8.5.3-alpha05"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
versionCode 180031
|
||||
versionName "1.8.5.3"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th', 'nb-rNO'
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -64,7 +64,8 @@ android {
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/META-INF/CHANGES"
|
||||
excludes += "/META-INF/README.md"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
@@ -93,7 +94,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.0-dev-k1.8.0-33c0ad36f83"
|
||||
kotlinCompilerExtensionVersion = "1.4.0"
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
@@ -144,6 +145,8 @@ dependencies {
|
||||
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp-coroutines'
|
||||
|
||||
implementation 'com.squareup.okio:okio-jvm:3.3.0'
|
||||
|
||||
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
@@ -151,7 +154,8 @@ dependencies {
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
implementation 'io.coil-kt:coil:2.2.2'
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
}
|
||||
|
||||
+6
-11
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -14,14 +15,8 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:tint="@color/brown_500"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_cookie" />
|
||||
<resources>
|
||||
|
||||
</vector>
|
||||
<string name="app_name" translatable="false">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -34,7 +34,7 @@
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.LoginActivity"
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
@@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
|
||||
add_subdirectory(libarchive)
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifUtils.c)
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
|
||||
@@ -1,4 +1,13 @@
|
||||
#include <jni.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define LOG_TAG "mmap_utils"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
@@ -13,4 +22,22 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
|
||||
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
||||
if (addr != MAP_FAILED) {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, capability);
|
||||
} else {
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#include <string.h>
|
||||
#include <jni.h>
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_Native_isGif(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOG_TAG "gifUtils"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
|
||||
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
||||
|
||||
typedef signed char byte;
|
||||
|
||||
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
|
||||
#define MINIMUM_FRAME_DELAY 2
|
||||
#define DEFAULT_FRAME_DELAY 10
|
||||
|
||||
static inline bool isGif(void *addr) {
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (size < 7 || !isGif(addr)) return;
|
||||
for (size_t i = 0; i < size - 8; i++) {
|
||||
// TODO: Optimize this hex find with SIMD?
|
||||
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
|
||||
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
|
||||
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
|
||||
byte *end = addr + i + 4;
|
||||
if (end[4] != 0) continue;
|
||||
int frameDelay = end[2] << 8 | end[1];
|
||||
if (frameDelay > MINIMUM_FRAME_DELAY)
|
||||
break; // Quit if the first block looks normal, for performance
|
||||
end[1] = DEFAULT_FRAME_DELAY;
|
||||
end[2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
@file:Suppress("SameParameterValue")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import okio.Buffer
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
/**
|
||||
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
|
||||
* below a threshold.
|
||||
*/
|
||||
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
|
||||
// An intermediary buffer so we can read and alter the data before it's written to the destination.
|
||||
private val buffer = Buffer()
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
// Ensure our buffer has enough bytes to satisfy this read.
|
||||
request(byteCount)
|
||||
|
||||
// Short circuit if there are no bytes in the buffer.
|
||||
if (buffer.size == 0L) {
|
||||
return if (byteCount == 0L) 0L else -1L
|
||||
}
|
||||
|
||||
// Search through the buffer and rewrite any frame delays below the threshold.
|
||||
var bytesWritten = 0L
|
||||
while (true) {
|
||||
val index = indexOf(FRAME_DELAY_START_MARKER)
|
||||
if (index == -1L) break
|
||||
|
||||
// Write up until the end of the frame delay start marker.
|
||||
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
|
||||
|
||||
// Check for the end of the graphics control extension block.
|
||||
if (!request(5) || buffer[4] != 0.toByte()) continue
|
||||
|
||||
// Rewrite the frame delay if it is below the threshold.
|
||||
// The frame delay is stored as two unsigned bytes in reverse order
|
||||
// (i.e. the most significant bits are in the byte that's read second).
|
||||
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
|
||||
if (frameDelay < MINIMUM_FRAME_DELAY) {
|
||||
sink.writeByte(buffer[0].toInt())
|
||||
sink.writeByte(DEFAULT_FRAME_DELAY)
|
||||
sink.writeByte(0)
|
||||
buffer.skip(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Write anything left in the source.
|
||||
if (bytesWritten < byteCount) {
|
||||
bytesWritten += write(sink, byteCount - bytesWritten)
|
||||
}
|
||||
return if (bytesWritten == 0L) -1 else bytesWritten
|
||||
}
|
||||
|
||||
private fun indexOf(bytes: ByteString): Long {
|
||||
var index = -1L
|
||||
while (true) {
|
||||
index = buffer.indexOf(bytes[0], index + 1)
|
||||
if (index == -1L) break
|
||||
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun write(sink: Buffer, byteCount: Long): Long {
|
||||
return buffer.read(sink, byteCount).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun request(byteCount: Long): Boolean {
|
||||
if (buffer.size >= byteCount) return true
|
||||
val toRead = byteCount - buffer.size
|
||||
return super.read(buffer, toRead) == toRead
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
// See: "Graphics Control Extension"
|
||||
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
|
||||
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
|
||||
private const val MINIMUM_FRAME_DELAY = 2
|
||||
private const val DEFAULT_FRAME_DELAY = 10
|
||||
}
|
||||
}
|
||||
@@ -476,10 +476,8 @@ 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 {
|
||||
// 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) {
|
||||
if (panEnabled && isReady && vTranslate != 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
|
||||
@@ -607,12 +605,16 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
return true
|
||||
}
|
||||
// Detect flings, taps and double taps
|
||||
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
|
||||
isZooming = false
|
||||
isPanning = false
|
||||
maxTouchCount = 0
|
||||
return true
|
||||
// May throw NPE
|
||||
runCatching {
|
||||
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
|
||||
isZooming = false
|
||||
isPanning = false
|
||||
maxTouchCount = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (vTranslateStart == null) {
|
||||
vTranslateStart = PointF(0F, 0F)
|
||||
}
|
||||
|
||||
@@ -28,5 +28,9 @@ object Native {
|
||||
@JvmStatic
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
|
||||
external fun isGif(buffer: ByteBuffer): Boolean
|
||||
}
|
||||
@JvmStatic
|
||||
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
|
||||
|
||||
@JvmStatic
|
||||
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package com.hippo
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
@@ -28,7 +29,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
}
|
||||
|
||||
private external fun openArchive(fd: Int, size: Long): Int
|
||||
external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
private external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
external fun extractToFd(index: Int, fd: Int)
|
||||
external fun getFilename(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
@@ -39,8 +40,23 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.ByteBufferSource? {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return null
|
||||
check(buffer.isDirect)
|
||||
return object : Image.ByteBufferSource {
|
||||
override fun getByteBuffer(): ByteBuffer {
|
||||
return buffer
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
@@ -1,985 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Each key must match
|
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
|
||||
* accessible as streams or files. Each value must be between {@code 0} and
|
||||
* {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <p>The cache stores its data in a directory on the filesystem. This
|
||||
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
|
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
/*
|
||||
* This cache uses a journal file named "journal". A typical journal file
|
||||
* looks like this:
|
||||
* libcore.io.DiskLruCache
|
||||
* 1
|
||||
* 100
|
||||
* 2
|
||||
*
|
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
*
|
||||
* The first five lines of the journal form its header. They are the
|
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
* the application's version, the value count, and a blank line.
|
||||
*
|
||||
* Each of the subsequent lines in the file is a record of the state of a
|
||||
* cache entry. Each line contains space-separated values: a state, a key,
|
||||
* and optional state-specific values.
|
||||
* o DIRTY lines track that an entry is actively being created or updated.
|
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
* temporary files may need to be deleted.
|
||||
* o CLEAN lines track a cache entry that has been successfully published
|
||||
* and may be read. A publish line is followed by the lengths of each of
|
||||
* its values.
|
||||
* o READ lines track accesses for LRU.
|
||||
* o REMOVE lines track entries that have been deleted.
|
||||
*
|
||||
* The journal file is appended to as cache operations occur. The journal may
|
||||
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
* it exists when the cache is opened.
|
||||
*/
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// Eat all writes silently. Nom nom.
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This cache uses a single background thread to evict entries.
|
||||
*/
|
||||
final ThreadPoolExecutor executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final File journalFileBackup;
|
||||
private final int appVersion;
|
||||
private final int valueCount;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<>(0, 0.75f, true);
|
||||
private long maxSize;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private int redundantOpCount;
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0;
|
||||
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
if (backupFile.exists()) {
|
||||
File journalFile = new File(directory, JOURNAL_FILE);
|
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) {
|
||||
backupFile.delete();
|
||||
} else {
|
||||
renameTo(backupFile, journalFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
System.out
|
||||
.println("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||
if (deleteDestination) {
|
||||
deleteIfExists(to);
|
||||
}
|
||||
if (!from.renameTo(to)) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic)
|
||||
|| !VERSION_1.equals(version)
|
||||
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||
|| !"".equals(blank)) {
|
||||
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||
+ valueCountString + ", " + blank + "]");
|
||||
}
|
||||
|
||||
int lineCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
lineCount++;
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
redundantOpCount = lineCount - lruEntries.size();
|
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) {
|
||||
rebuildJournal();
|
||||
} else {
|
||||
journalWriter = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
int keyBegin = firstSpace + 1;
|
||||
int secondSpace = line.indexOf(' ', keyBegin);
|
||||
final String key;
|
||||
if (secondSpace == -1) {
|
||||
key = line.substring(keyBegin);
|
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = line.substring(keyBegin, secondSpace);
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(parts);
|
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||
// This work was already done by calling lruEntries.get().
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
try (Writer writer = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (journalFile.exists()) {
|
||||
renameTo(journalFile, journalFileBackup, true);
|
||||
}
|
||||
renameTo(journalFileTmp, journalFile, false);
|
||||
journalFileBackup.delete();
|
||||
|
||||
journalWriter = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
} private final Callable<Void> cleanupCallable = new Callable<>() {
|
||||
public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // Closed.
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return {@code true} if it contains the key
|
||||
*/
|
||||
public synchronized boolean contain(String key) {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
return entry != null && entry.readable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (ins[i] != null) {
|
||||
Util.closeQuietly(ins[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // Snapshot is stale.
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // Another edit is in progress.
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where this cache stores its data.
|
||||
*/
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public synchronized long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum number of bytes the cache can store and queues a job
|
||||
* to trim the existing store, if necessary.
|
||||
*/
|
||||
public synchronized void setMaxSize(long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
journalWriter.flush();
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cache has been closed.
|
||||
*/
|
||||
public synchronized boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force buffered operations to the filesystem.
|
||||
*/
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this cache. Stored values will remain on the filesystem.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // Already closed.
|
||||
}
|
||||
for (Entry entry : new ArrayList<>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache and deletes all of its stored values. This will delete
|
||||
* all files in the cache directory including files that weren't created by
|
||||
* the cache.
|
||||
*/
|
||||
public void delete() throws IOException {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("keys must match regex "
|
||||
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the values for an entry.
|
||||
*/
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
private final long[] lengths;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
this.lengths = lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbuffered stream with the value for {@code index}.
|
||||
*/
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of the value for {@code index}.
|
||||
*/
|
||||
public long getLength(int index) {
|
||||
return lengths[index];
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
private boolean committed;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
if (index < 0 || index >= valueCount) {
|
||||
throw new IllegalArgumentException("Expected index " + index + " to "
|
||||
+ "be greater than 0 and less than the maximum value count "
|
||||
+ "of " + valueCount);
|
||||
}
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
File dirtyFile = entry.getDirtyFile(index);
|
||||
FileOutputStream outputStream;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs();
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e2) {
|
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM;
|
||||
}
|
||||
}
|
||||
return new FaultHidingOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at {@code index} to {@code value}.
|
||||
*/
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
public void abortUnlessCommitted() {
|
||||
if (!committed) {
|
||||
try {
|
||||
abort();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Lengths of this entry's files.
|
||||
*/
|
||||
private final long[] lengths;
|
||||
|
||||
/**
|
||||
* True if this entry has ever been published.
|
||||
*/
|
||||
private boolean readable;
|
||||
|
||||
/**
|
||||
* The ongoing edit or null if this entry is not being edited.
|
||||
*/
|
||||
private Editor currentEditor;
|
||||
|
||||
/**
|
||||
* The sequence number of the most recently committed edit to this entry.
|
||||
*/
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,504 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class SimpleDiskCache {
|
||||
|
||||
private static final String TAG = SimpleDiskCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Disk cache is free
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Disk cache is flushing or clearing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_IN_USE = 1;
|
||||
|
||||
/**
|
||||
* Disk cache is reading or writing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_BUSY = 2;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mSize;
|
||||
@NonNull
|
||||
private final Map<String, CounterLock> mLockMap = new HashMap<>();
|
||||
@NonNull
|
||||
private final LockPool mLockPool = new LockPool();
|
||||
private int mDiskCacheState;
|
||||
@Nullable
|
||||
private DiskLruCache mDiskLruCache;
|
||||
|
||||
public SimpleDiskCache(File cacheDir, int size) {
|
||||
mCacheDir = cacheDir;
|
||||
mSize = size;
|
||||
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
private static String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = HashCodeUtils.bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
private synchronized void init() throws IOException {
|
||||
if (!isValid()) {
|
||||
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long size() {
|
||||
if (null != mDiskLruCache) {
|
||||
return mDiskLruCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
if (null != mDiskLruCache) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache flush", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized boolean clear() {
|
||||
boolean result = true;
|
||||
if (null == mDiskLruCache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache clearCache", e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache init", e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized CounterLock obtainLock(String key) {
|
||||
// Wait for clear over
|
||||
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
|
||||
|
||||
// Get lock from key
|
||||
CounterLock lock;
|
||||
lock = mLockMap.get(key);
|
||||
if (lock == null) {
|
||||
lock = mLockPool.get();
|
||||
mLockMap.put(key, lock);
|
||||
}
|
||||
lock.obtain();
|
||||
|
||||
return lock;
|
||||
}
|
||||
|
||||
private synchronized void releaseLock(String key, CounterLock lock) {
|
||||
lock.release();
|
||||
if (lock.isFree()) {
|
||||
mLockMap.remove(key);
|
||||
mLockPool.push(lock);
|
||||
}
|
||||
|
||||
if (mLockMap.isEmpty()) {
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.readLock().lock();
|
||||
|
||||
boolean result = mDiskLruCache.contain(diskKey);
|
||||
|
||||
lock.readLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result;
|
||||
try {
|
||||
result = mDiskLruCache.remove(diskKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean put(@NonNull String key, @NonNull InputStream is) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result = false;
|
||||
if (isValid()) {
|
||||
result = putToDisk(diskKey, is);
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean putToDisk(String key, InputStream is) {
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream os = null;
|
||||
boolean completeEdit = false;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
editor = mDiskLruCache.edit(key);
|
||||
if (editor == null) {
|
||||
// The editor is in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
os = editor.newOutputStream(0);
|
||||
if (os != null) {
|
||||
Util.copy(is, os);
|
||||
completeEdit = true;
|
||||
editor.commit();
|
||||
return true;
|
||||
} else {
|
||||
// Can't get OutputStream
|
||||
completeEdit = true;
|
||||
editor.abort();
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(os);
|
||||
try {
|
||||
if (!completeEdit && editor != null) {
|
||||
editor.abort();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key of the target
|
||||
* @return the InputStreamPipe, <code>null</code> for missing
|
||||
*/
|
||||
@Nullable
|
||||
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
|
||||
if (contain(key)) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheInputStreamPipe(diskKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheOutputStreamPipe(diskKey);
|
||||
}
|
||||
|
||||
private class CacheInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Snapshot mCurrentSnapshot;
|
||||
|
||||
private CacheInputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.readLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
InputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
snapshot = mDiskLruCache.get(mKey);
|
||||
if (snapshot == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
mCurrentSnapshot = snapshot;
|
||||
return snapshot.getInputStream(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Util.closeQuietly(mCurrentSnapshot);
|
||||
mCurrentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Editor mCurrentEditor;
|
||||
|
||||
private CacheOutputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.writeLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
OutputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
|
||||
if (editor == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
|
||||
OutputStream os;
|
||||
try {
|
||||
os = editor.newOutputStream(0);
|
||||
mCurrentEditor = editor;
|
||||
return os;
|
||||
} catch (IOException e) {
|
||||
// Get trouble
|
||||
try {
|
||||
editor.abort();
|
||||
} catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCurrentEditor != null) {
|
||||
try {
|
||||
mCurrentEditor.commit();
|
||||
} catch (IOException e) {
|
||||
// Empty
|
||||
}
|
||||
mCurrentEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CounterLock extends ReentrantReadWriteLock {
|
||||
|
||||
private int mCount;
|
||||
|
||||
public boolean isFree() {
|
||||
return mCount == 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (--mCount < 0) {
|
||||
throw new IllegalStateException("Release time is more than occupy time");
|
||||
}
|
||||
}
|
||||
|
||||
public void obtain() {
|
||||
++mCount;
|
||||
}
|
||||
}
|
||||
|
||||
private class LockPool {
|
||||
|
||||
private static final int MAX_SIZE = 10;
|
||||
|
||||
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
|
||||
private final int mMaxSize = MAX_SIZE;
|
||||
private int mSize;
|
||||
|
||||
public void push(CounterLock lock) {
|
||||
if (lock != null && mSize < mMaxSize) {
|
||||
mArray[mSize++] = lock;
|
||||
}
|
||||
}
|
||||
|
||||
public CounterLock get() {
|
||||
if (mSize > 0) {
|
||||
CounterLock lock = mArray[--mSize];
|
||||
mArray[mSize] = null;
|
||||
return lock;
|
||||
} else {
|
||||
return new CounterLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
|
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
|
||||
* line at end of input is invalid and will be ignored, the caller may use {@code
|
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||
* end-of-input reporting and a more restrictive definition of a line.
|
||||
*
|
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
/*
|
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
*/
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(Util.US_ASCII))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset);
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
return new String(buf, 0, length, charset);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasUnterminatedLine() {
|
||||
return end == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Junk drawer of utility methods.
|
||||
*/
|
||||
final class Util {
|
||||
static final Charset US_ASCII = StandardCharsets.US_ASCII;
|
||||
static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
static String readFully(Reader reader) throws IOException {
|
||||
try (reader) {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||
*/
|
||||
static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IOException("not a readable directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy bytes from an <code>InputStream</code> to an
|
||||
* <code>OutputStream</code>.
|
||||
*
|
||||
* @param input the InputStream
|
||||
* @param output the OutputStream
|
||||
* @return the number of bytes copied
|
||||
*/
|
||||
public static long copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,7 @@ public class Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(String host) {
|
||||
public boolean contains(String host) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
|
||||
return cursor.moveToNext();
|
||||
}
|
||||
|
||||
@@ -85,13 +85,12 @@ 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_BYPASS_VPN = "bypass_vpn";
|
||||
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;
|
||||
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||
private static final String KEY_PREVIEW_NUM = "preview_num";
|
||||
private static final int DEFAULT_PREVIEW_NUM = 60;
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||
private static final String DEFAULT_DISPLAY_NAME = null;
|
||||
@@ -181,6 +180,7 @@ 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 boolean DEFAULT_BYPASS_VPN = true;
|
||||
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";
|
||||
@@ -423,10 +423,6 @@ public class Settings {
|
||||
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
|
||||
}
|
||||
|
||||
public static int getPreviewNum() {
|
||||
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
|
||||
}
|
||||
|
||||
public static int getDefaultCategories() {
|
||||
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
|
||||
}
|
||||
@@ -679,6 +675,10 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static boolean getBypassVPN() {
|
||||
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||
}
|
||||
|
||||
public static int getProxyType() {
|
||||
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
|
||||
}
|
||||
|
||||
@@ -123,4 +123,8 @@ object EhDns : Dns {
|
||||
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
|
||||
?: Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInHosts(hostname: String): Boolean {
|
||||
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getGalleryList(url: String): GalleryListParser.Result {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -279,8 +279,8 @@ object EhEngine {
|
||||
}
|
||||
json.put("gidlist", ja)
|
||||
json.put("namespace", 1)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(json.toString().toRequestBody(MEDIA_TYPE_JSON))
|
||||
@@ -306,7 +306,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getGalleryDetail(url: String?): GalleryDetail {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -334,7 +334,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getPreviewSet(url: String?): Pair<PreviewSet, Int> {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -372,9 +372,9 @@ object EhEngine {
|
||||
json.put("token", token)
|
||||
json.put("rating", ceil((rating * 2).toDouble()).toInt())
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -409,7 +409,7 @@ object EhEngine {
|
||||
builder.add("commenttext_edit", comment)
|
||||
builder.add("edit_comment", id)
|
||||
}
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -451,9 +451,9 @@ object EhEngine {
|
||||
)
|
||||
)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -480,7 +480,7 @@ object EhEngine {
|
||||
suspend fun getFavorites(
|
||||
url: String
|
||||
): FavoritesParser.Result {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -534,7 +534,7 @@ object EhEngine {
|
||||
builder.add("submit", "Apply Changes")
|
||||
builder.add("update", "1")
|
||||
val url = EhUrl.getAddFavorites(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -597,7 +597,7 @@ object EhEngine {
|
||||
builder.add("modifygids[]", gid.toString())
|
||||
}
|
||||
builder.add("apply", "Apply")
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -669,7 +669,14 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
result = ArchiveParser.parse(body)
|
||||
result = ArchiveParser.parse(body!!)!!
|
||||
if (result.funds == null) {
|
||||
try {
|
||||
result.funds = getFunds()
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -703,7 +710,7 @@ object EhEngine {
|
||||
}
|
||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(builder.build())
|
||||
@@ -711,43 +718,68 @@ object EhEngine {
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var result: String?
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
result = ArchiveParser.parseArchiveUrl(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
if (!isHAtH) {
|
||||
if (result == null) {
|
||||
// Wait for the server to prepare archives
|
||||
delay(1000)
|
||||
try {
|
||||
call.clone().executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
result = ArchiveParser.parseArchiveUrl(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
if (result == null) {
|
||||
throw EhException("Archive unavailable")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getFunds(): ArchiveParser.Funds? {
|
||||
val url = EhUrl.URL_FUNDS
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
return ArchiveParser.parseFunds(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
var result = ArchiveParser.parseArchiveUrl(body)
|
||||
if (!isHAtH) {
|
||||
if (result == null) {
|
||||
delay(1000)
|
||||
try {
|
||||
call.clone().executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
result = ArchiveParser.parseArchiveUrl(body)
|
||||
if (result == null) {
|
||||
throw EhException("Archive unavailable")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
@@ -799,7 +831,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getUConfig(): Void? {
|
||||
val url = EhUrl.getUConfigUrl()
|
||||
val url = EhUrl.uConfigUrl
|
||||
Log.d(TAG, url)
|
||||
var request = EhRequestBuilder(url, null).build()
|
||||
var call = okHttpClient.newCall(request)
|
||||
@@ -855,9 +887,9 @@ object EhEngine {
|
||||
json.put("comment_id", commentId)
|
||||
json.put("comment_vote", commentVote)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -894,9 +926,9 @@ object EhEngine {
|
||||
json.put("tags", tags)
|
||||
json.put("vote", vote)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -958,9 +990,9 @@ object EhEngine {
|
||||
Headers.headersOf("Content-Disposition", "form-data; name=\"f_sfile\""),
|
||||
"File Search".toRequestBody()
|
||||
)
|
||||
val url = EhUrl.getImageSearchUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.imageSearchUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(builder.build())
|
||||
@@ -1032,12 +1064,12 @@ object EhEngine {
|
||||
json.put("imgkey", pToken)
|
||||
json.put("showkey", showKey)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val url = EhUrl.apiUrl
|
||||
var referer: String? = null
|
||||
if (index > 0 && previousPToken != null) {
|
||||
referer = EhUrl.getPageUrl(gid, index - 1, previousPToken)
|
||||
}
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
|
||||
@@ -34,8 +34,8 @@ object EhSSLSocketFactory : SSLSocketFactory() {
|
||||
}
|
||||
|
||||
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||
val address = s.inetAddress.hostAddress
|
||||
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
|
||||
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
|
||||
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
|
||||
val sslSession = socket.session
|
||||
Log.d(
|
||||
"EhSSLSocketFactory",
|
||||
|
||||
@@ -1,196 +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.client;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.network.UrlBuilder;
|
||||
|
||||
public class EhUrl {
|
||||
|
||||
public static final int SITE_E = 0;
|
||||
public static final int SITE_EX = 1;
|
||||
|
||||
public static final String DOMAIN_EX = "exhentai.org";
|
||||
public static final String DOMAIN_E = "e-hentai.org";
|
||||
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
|
||||
|
||||
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
|
||||
public static final String API_EX = HOST_EX + "api.php";
|
||||
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
|
||||
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
|
||||
public static final String URL_WATCHED_EX = HOST_EX + "watched";
|
||||
public static final String HOST_E = "https://" + DOMAIN_E + "/";
|
||||
public static final String API_E = HOST_E + "api.php";
|
||||
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
|
||||
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
|
||||
public static final String URL_WATCHED_E = HOST_E + "watched";
|
||||
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
|
||||
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
|
||||
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
|
||||
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
|
||||
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
|
||||
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
|
||||
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
|
||||
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
|
||||
public static final String REFERER_EX = "https://" + DOMAIN_EX;
|
||||
public static final String ORIGIN_EX = REFERER_EX;
|
||||
public static final String REFERER_E = "https://" + DOMAIN_E;
|
||||
public static final String ORIGIN_E = REFERER_E;
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token) {
|
||||
return getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
public static String getHost() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return HOST_E;
|
||||
case SITE_EX:
|
||||
return HOST_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFavoritesUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_FAVORITES_E;
|
||||
case SITE_EX:
|
||||
return URL_FAVORITES_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getApiUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return API_E;
|
||||
case SITE_EX:
|
||||
return API_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getReferer() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return REFERER_E;
|
||||
case SITE_EX:
|
||||
return REFERER_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getOrigin() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return ORIGIN_E;
|
||||
case SITE_EX:
|
||||
return ORIGIN_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getUConfigUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_UCONFIG_E;
|
||||
case SITE_EX:
|
||||
return URL_UCONFIG_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMyTagsUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_MY_TAGS_E;
|
||||
case SITE_EX:
|
||||
return URL_MY_TAGS_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/');
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index);
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getGalleryMultiPageViewerUrl(long gid, String token) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "mpv/" + gid + '/' + token + '/');
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getPageUrl(long gid, int index, String pToken) {
|
||||
return getHost() + "s/" + pToken + '/' + gid + '-' + (index + 1);
|
||||
}
|
||||
|
||||
public static String getAddFavorites(long gid, String token) {
|
||||
return getHost() + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav";
|
||||
}
|
||||
|
||||
public static String getDownloadArchive(long gid, String token, String or) {
|
||||
return getHost() + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or;
|
||||
}
|
||||
|
||||
public static String getTagDefinitionUrl(String tag) {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_');
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getPopularUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_POPULAR_E;
|
||||
case SITE_EX:
|
||||
return URL_POPULAR_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getImageSearchUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_IMAGE_SEARCH_E;
|
||||
case SITE_EX:
|
||||
return URL_IMAGE_SEARCH_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getWatchedUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_WATCHED_E;
|
||||
case SITE_EX:
|
||||
return URL_WATCHED_EX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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.client
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.network.UrlBuilder
|
||||
|
||||
object EhUrl {
|
||||
const val SITE_E = 0
|
||||
const val SITE_EX = 1
|
||||
const val DOMAIN_EX = "exhentai.org"
|
||||
const val DOMAIN_E = "e-hentai.org"
|
||||
const val DOMAIN_LOFI = "lofi.e-hentai.org"
|
||||
const val HOST_EX = "https://$DOMAIN_EX/"
|
||||
const val API_EX = HOST_EX + "api.php"
|
||||
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
|
||||
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
|
||||
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
|
||||
const val URL_WATCHED_EX = HOST_EX + "watched"
|
||||
const val HOST_E = "https://$DOMAIN_E/"
|
||||
const val API_E = HOST_E + "api.php"
|
||||
const val URL_FAVORITES_E = HOST_E + "favorites.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val URL_WATCHED_E = HOST_E + "watched"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
|
||||
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
|
||||
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
|
||||
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
|
||||
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||
const val ORIGIN_EX = REFERER_EX
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
const val ORIGIN_E = REFERER_E
|
||||
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
||||
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
|
||||
|
||||
val host: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> HOST_E
|
||||
SITE_EX -> HOST_EX
|
||||
else -> HOST_E
|
||||
}
|
||||
|
||||
val favoritesUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_FAVORITES_E
|
||||
SITE_EX -> URL_FAVORITES_EX
|
||||
else -> URL_FAVORITES_E
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val referer: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> REFERER_E
|
||||
SITE_EX -> REFERER_EX
|
||||
else -> REFERER_E
|
||||
}
|
||||
|
||||
val origin: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> ORIGIN_E
|
||||
SITE_EX -> ORIGIN_EX
|
||||
else -> ORIGIN_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val uConfigUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_UCONFIG_E
|
||||
SITE_EX -> URL_UCONFIG_EX
|
||||
else -> URL_UCONFIG_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val myTagsUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_MY_TAGS_E
|
||||
SITE_EX -> URL_MY_TAGS_EX
|
||||
else -> URL_MY_TAGS_E
|
||||
}
|
||||
|
||||
val popularUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_POPULAR_E
|
||||
SITE_EX -> URL_POPULAR_EX
|
||||
else -> URL_POPULAR_E
|
||||
}
|
||||
|
||||
val imageSearchUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_IMAGE_SEARCH_E
|
||||
SITE_EX -> URL_IMAGE_SEARCH_EX
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
val watchedUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_WATCHED_E
|
||||
SITE_EX -> URL_WATCHED_EX
|
||||
else -> URL_WATCHED_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||
return getGalleryDetailUrl(gid, token, 0, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
|
||||
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index)
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
|
||||
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
|
||||
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||
}
|
||||
|
||||
fun getAddFavorites(gid: Long, token: String?): String {
|
||||
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||
}
|
||||
|
||||
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
|
||||
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||
}
|
||||
|
||||
fun getTagDefinitionUrl(tag: String): String {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||
}
|
||||
|
||||
fun getThumbUrlPrefix(): String {
|
||||
return when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_PREFIX_THUMB_E
|
||||
SITE_EX -> URL_PREFIX_THUMB_EX
|
||||
else -> URL_PREFIX_THUMB_E
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +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.client;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EhUtils {
|
||||
|
||||
public static final int NONE = -1; // Use it for homepage
|
||||
public static final int PRIVATE = 0x400;
|
||||
public static final int UNKNOWN = 0x800;
|
||||
|
||||
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
|
||||
public static final int BG_COLOR_MANGA = 0xffff9800;
|
||||
public static final int BG_COLOR_ARTIST_CG = 0xfffbc02d;
|
||||
public static final int BG_COLOR_GAME_CG = 0xff4caf50;
|
||||
public static final int BG_COLOR_WESTERN = 0xff8bc34a;
|
||||
public static final int BG_COLOR_NON_H = 0xff2196f3;
|
||||
public static final int BG_COLOR_IMAGE_SET = 0xff3f51b5;
|
||||
public static final int BG_COLOR_COSPLAY = 0xff9c27b0;
|
||||
public static final int BG_COLOR_ASIAN_PORN = 0xff9575cd;
|
||||
public static final int BG_COLOR_MISC = 0xfff06292;
|
||||
public static final int BG_COLOR_UNKNOWN = Color.BLACK;
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
public static final Pattern PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*");
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
public static final Pattern PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final int[] CATEGORY_VALUES = {
|
||||
EhConfig.MISC,
|
||||
EhConfig.DOUJINSHI,
|
||||
EhConfig.MANGA,
|
||||
EhConfig.ARTIST_CG,
|
||||
EhConfig.GAME_CG,
|
||||
EhConfig.IMAGE_SET,
|
||||
EhConfig.COSPLAY,
|
||||
EhConfig.ASIAN_PORN,
|
||||
EhConfig.NON_H,
|
||||
EhConfig.WESTERN,
|
||||
PRIVATE,
|
||||
UNKNOWN};
|
||||
|
||||
private static final String[][] CATEGORY_STRINGS = {
|
||||
new String[]{"misc"},
|
||||
new String[]{"doujinshi"},
|
||||
new String[]{"manga"},
|
||||
new String[]{"artistcg", "Artist CG Sets", "Artist CG"},
|
||||
new String[]{"gamecg", "Game CG Sets", "Game CG"},
|
||||
new String[]{"imageset", "Image Sets", "Image Set"},
|
||||
new String[]{"cosplay"},
|
||||
new String[]{"asianporn", "Asian Porn"},
|
||||
new String[]{"non-h"},
|
||||
new String[]{"western"},
|
||||
new String[]{"private"},
|
||||
new String[]{"unknown"}
|
||||
};
|
||||
|
||||
public static int getCategory(String type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) {
|
||||
for (String str : CATEGORY_STRINGS[i])
|
||||
if (str.equalsIgnoreCase(type))
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getCategory(int type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
|
||||
if (CATEGORY_VALUES[i] == type)
|
||||
break;
|
||||
}
|
||||
return CATEGORY_STRINGS[i][0];
|
||||
}
|
||||
|
||||
public static int getCategoryColor(int category) {
|
||||
return switch (category) {
|
||||
case EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI;
|
||||
case EhConfig.MANGA -> BG_COLOR_MANGA;
|
||||
case EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG;
|
||||
case EhConfig.GAME_CG -> BG_COLOR_GAME_CG;
|
||||
case EhConfig.WESTERN -> BG_COLOR_WESTERN;
|
||||
case EhConfig.NON_H -> BG_COLOR_NON_H;
|
||||
case EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET;
|
||||
case EhConfig.COSPLAY -> BG_COLOR_COSPLAY;
|
||||
case EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN;
|
||||
case EhConfig.MISC -> BG_COLOR_MISC;
|
||||
default -> BG_COLOR_UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
public static void signOut() {
|
||||
EhApplication.getEhCookieStore().signOut();
|
||||
Settings.putAvatar(null);
|
||||
Settings.putDisplayName(null);
|
||||
Settings.putNeedSignIn(true);
|
||||
}
|
||||
|
||||
public static boolean needSignedIn() {
|
||||
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
|
||||
}
|
||||
|
||||
public static String getSuitableTitle(GalleryInfo gi) {
|
||||
if (Settings.getShowJpnTitle()) {
|
||||
return TextUtils.isEmpty(gi.getTitleJpn()) ? gi.getTitle() : gi.getTitleJpn();
|
||||
} else {
|
||||
return TextUtils.isEmpty(gi.getTitle()) ? gi.getTitleJpn() : gi.getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String extractTitle(String title) {
|
||||
if (null == title) {
|
||||
return null;
|
||||
}
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("");
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("");
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
int index = title.indexOf('|');
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index);
|
||||
}
|
||||
if (title.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
public static String handleThumbUrlResolution(String url) {
|
||||
if (null == url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String resolution;
|
||||
switch (Settings.getThumbResolution()) {
|
||||
default:
|
||||
case 0: // Auto
|
||||
return url;
|
||||
case 1: // 250
|
||||
resolution = "250";
|
||||
break;
|
||||
case 2: // 300
|
||||
resolution = "300";
|
||||
break;
|
||||
}
|
||||
|
||||
int index1 = url.lastIndexOf('_');
|
||||
int index2 = url.lastIndexOf('.');
|
||||
if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||
return url.substring(0, index1 + 1) + resolution + url.substring(index2);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.client
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object EhUtils {
|
||||
const val NONE = -1 // Use it for homepage
|
||||
const val PRIVATE = 0x400
|
||||
const val UNKNOWN = 0x800
|
||||
|
||||
// https://youtrack.jetbrains.com/issue/KT-4749
|
||||
private const val BG_COLOR_DOUJINSHI = 0xfff44336u
|
||||
private const val BG_COLOR_MANGA = 0xffff9800u
|
||||
private const val BG_COLOR_ARTIST_CG = 0xfffbc02du
|
||||
private const val BG_COLOR_GAME_CG = 0xff4caf50u
|
||||
private const val BG_COLOR_WESTERN = 0xff8bc34au
|
||||
private const val BG_COLOR_NON_H = 0xff2196f3u
|
||||
private const val BG_COLOR_IMAGE_SET = 0xff3f51b5u
|
||||
private const val BG_COLOR_COSPLAY = 0xff9c27b0u
|
||||
private const val BG_COLOR_ASIAN_PORN = 0xff9575cdu
|
||||
private const val BG_COLOR_MISC = 0xfff06292u
|
||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*"
|
||||
)
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
)
|
||||
|
||||
private val CATEGORY_VALUES = hashMapOf(
|
||||
EhConfig.MISC to arrayOf("misc"),
|
||||
EhConfig.DOUJINSHI to arrayOf("doujinshi"),
|
||||
EhConfig.MANGA to arrayOf("manga"),
|
||||
EhConfig.ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
|
||||
EhConfig.GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
|
||||
EhConfig.IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
|
||||
EhConfig.COSPLAY to arrayOf("cosplay"),
|
||||
EhConfig.ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
|
||||
EhConfig.NON_H to arrayOf("non-h"),
|
||||
EhConfig.WESTERN to arrayOf("western"),
|
||||
PRIVATE to arrayOf("private"),
|
||||
UNKNOWN to arrayOf("unknown")
|
||||
)
|
||||
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
|
||||
|
||||
@JvmStatic
|
||||
fun getCategory(type: String?): Int {
|
||||
for (entry in CATEGORY_STRINGS) {
|
||||
for (str in entry.first)
|
||||
if (str.equals(type, ignoreCase = true))
|
||||
return entry.second
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
fun getCategory(type: Int): String {
|
||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||
}
|
||||
|
||||
fun getCategoryColor(category: Int): Int {
|
||||
return when (category) {
|
||||
EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI
|
||||
EhConfig.MANGA -> BG_COLOR_MANGA
|
||||
EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG
|
||||
EhConfig.GAME_CG -> BG_COLOR_GAME_CG
|
||||
EhConfig.WESTERN -> BG_COLOR_WESTERN
|
||||
EhConfig.NON_H -> BG_COLOR_NON_H
|
||||
EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET
|
||||
EhConfig.COSPLAY -> BG_COLOR_COSPLAY
|
||||
EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN
|
||||
EhConfig.MISC -> BG_COLOR_MISC
|
||||
else -> BG_COLOR_UNKNOWN
|
||||
}.toInt()
|
||||
}
|
||||
|
||||
fun signOut() {
|
||||
ehCookieStore.signOut()
|
||||
Settings.putAvatar(null)
|
||||
Settings.putDisplayName(null)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
Settings.putNeedSignIn(true)
|
||||
}
|
||||
|
||||
fun needSignedIn(): Boolean {
|
||||
return Settings.getNeedSignIn() && !ehCookieStore.hasSignedIn()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSuitableTitle(gi: GalleryInfo): String? {
|
||||
return if (Settings.getShowJpnTitle()) {
|
||||
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
|
||||
} else {
|
||||
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
|
||||
}
|
||||
}
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
var title: String = fullTitle ?: return null
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
val index = title.indexOf('|')
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index)
|
||||
}
|
||||
return title.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun fixThumbUrl(url: String): String {
|
||||
return EhUrl.getThumbUrlPrefix() +
|
||||
url.removePrefix(EhUrl.URL_PREFIX_THUMB_E).removePrefix(EhUrl.URL_PREFIX_THUMB_EX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
if (null == url) {
|
||||
return null
|
||||
}
|
||||
val resolution = when (Settings.getThumbResolution()) {
|
||||
0 -> return url
|
||||
1 -> "250"
|
||||
2 -> "300"
|
||||
else -> return url
|
||||
}
|
||||
val index1 = url.lastIndexOf('_')
|
||||
val index2 = url.lastIndexOf('.')
|
||||
return if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||
url.substring(0, index1 + 1) + resolution + url.substring(index2)
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class FavListUrlBuilder(
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
val ub = UrlBuilder(EhUrl.getFavoritesUrl())
|
||||
val ub = UrlBuilder(EhUrl.favoritesUrl)
|
||||
if (isValidFavCat(favCat)) {
|
||||
ub.addQuery("favcat", favCat.toString())
|
||||
} else if (favCat == FAV_CAT_ALL) {
|
||||
|
||||
@@ -275,9 +275,9 @@ data class ListUrlBuilder(
|
||||
return when (mode) {
|
||||
MODE_NORMAL, MODE_SUBSCRIPTION -> {
|
||||
val url: String = if (mode == MODE_NORMAL) {
|
||||
EhUrl.getHost()
|
||||
EhUrl.host
|
||||
} else {
|
||||
EhUrl.getWatchedUrl()
|
||||
EhUrl.watchedUrl
|
||||
}
|
||||
val ub = UrlBuilder(url)
|
||||
if (this.category != EhUtils.NONE) {
|
||||
@@ -326,7 +326,7 @@ data class ListUrlBuilder(
|
||||
}
|
||||
|
||||
MODE_UPLOADER -> {
|
||||
val sb = StringBuilder(EhUrl.getHost())
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("uploader/")
|
||||
sb.append(encodeUTF8(it))
|
||||
@@ -344,7 +344,7 @@ data class ListUrlBuilder(
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
val sb = StringBuilder(EhUrl.getHost())
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("tag/")
|
||||
sb.append(encodeUTF8(it))
|
||||
@@ -361,8 +361,8 @@ data class ListUrlBuilder(
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
|
||||
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||
MODE_TOPLIST -> {
|
||||
val sb = StringBuilder(EhUrl.HOST_E)
|
||||
sb.append("toplist.php?tl=")
|
||||
|
||||
@@ -1,208 +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.client.parser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ArchiveParser {
|
||||
|
||||
private static final Pattern PATTERN_ARCHIVE_URL = Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>");
|
||||
private static final Pattern PATTERN_HATH_FORM = Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">");
|
||||
private static final Pattern PATTERN_HATH_ARCHIVE = Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>");
|
||||
private static final Pattern PATTERN_NEED_HATH_CLIENT = Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.");
|
||||
|
||||
public static Result parse(String body) {
|
||||
Matcher m = PATTERN_HATH_FORM.matcher(body);
|
||||
if (!m.find()) {
|
||||
return new Result(null, null);
|
||||
}
|
||||
String paramOr = m.group(1);
|
||||
|
||||
var archiveList = new ArrayList<Archive>();
|
||||
var d = Jsoup.parse(body);
|
||||
var es = d.select("#db>div>div");
|
||||
for (var e : es) {
|
||||
try {
|
||||
var res = e.selectFirst("form>input").attr("value");
|
||||
var name = e.selectFirst("form>div>input").attr("value");
|
||||
var size = e.selectFirst("p>strong").text();
|
||||
var cost = e.selectFirst("div>strong").text();
|
||||
var item = new Archive(res, name, size, cost, false);
|
||||
archiveList.add(item);
|
||||
} catch (NullPointerException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
m = PATTERN_HATH_ARCHIVE.matcher(body);
|
||||
while (m.find()) {
|
||||
String res = ParserUtils.trim(m.group(1));
|
||||
String name = ParserUtils.trim(m.group(2));
|
||||
String size = ParserUtils.trim(m.group(3));
|
||||
String cost = ParserUtils.trim(m.group(4));
|
||||
var item = new Archive(res, name, size, cost, true);
|
||||
archiveList.add(item);
|
||||
}
|
||||
|
||||
return new Result(paramOr, archiveList);
|
||||
}
|
||||
|
||||
public static String parseArchiveUrl(String body) throws NoHAtHClientException {
|
||||
Matcher m = PATTERN_NEED_HATH_CLIENT.matcher(body);
|
||||
if (m.find()) {
|
||||
throw new NoHAtHClientException("No H@H client");
|
||||
}
|
||||
|
||||
var d = Jsoup.parse(body);
|
||||
var a = d.selectFirst("#continue>a[href]");
|
||||
if (a != null) {
|
||||
return a.attr("href") + "?start=1";
|
||||
}
|
||||
|
||||
// TODO: Check more errors
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final class Archive {
|
||||
private final String res;
|
||||
private final String name;
|
||||
private final String size;
|
||||
private final String cost;
|
||||
private final boolean isHAtH;
|
||||
|
||||
public Archive(String res, String name, String size, String cost, boolean isHAtH) {
|
||||
this.res = res;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.cost = cost;
|
||||
this.isHAtH = isHAtH;
|
||||
}
|
||||
|
||||
public String format(IntFunction<String> getString) {
|
||||
if (isHAtH) {
|
||||
var costStr = cost.equals("Free") ? getString.apply(R.string.archive_free) : cost;
|
||||
return String.format("[H@H] %s [%s] [%s]", name, size, costStr);
|
||||
} else {
|
||||
var nameStr = getString.apply(res.equals("org") ? R.string.archive_original : R.string.archive_resample);
|
||||
var costStr = cost.equals("Free!") ? getString.apply(R.string.archive_free) : cost;
|
||||
return String.format("%s [%s] [%s]", nameStr, size, costStr);
|
||||
}
|
||||
}
|
||||
|
||||
public String res() {
|
||||
return res;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public String cost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public boolean isHAtH() {
|
||||
return isHAtH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (Archive) obj;
|
||||
return Objects.equals(this.res, that.res) &&
|
||||
Objects.equals(this.name, that.name) &&
|
||||
Objects.equals(this.size, that.size) &&
|
||||
Objects.equals(this.cost, that.cost) &&
|
||||
this.isHAtH == that.isHAtH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(res, name, size, cost, isHAtH);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Archive[" +
|
||||
"res=" + res + ", " +
|
||||
"name=" + name + ", " +
|
||||
"size=" + size + ", " +
|
||||
"cost=" + cost + ", " +
|
||||
"isHAtH=" + isHAtH + ']';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
private final String paramOr;
|
||||
private final List<Archive> archiveList;
|
||||
|
||||
public Result(String paramOr, List<Archive> archiveList) {
|
||||
this.paramOr = paramOr;
|
||||
this.archiveList = archiveList;
|
||||
}
|
||||
|
||||
public String paramOr() {
|
||||
return paramOr;
|
||||
}
|
||||
|
||||
public List<Archive> archiveList() {
|
||||
return archiveList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (Result) obj;
|
||||
return Objects.equals(this.paramOr, that.paramOr) &&
|
||||
Objects.equals(this.archiveList, that.archiveList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(paramOr, archiveList);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Result[" +
|
||||
"paramOr=" + paramOr + ", " +
|
||||
"archiveList=" + archiveList + ']';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.client.parser
|
||||
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.function.IntFunction
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ArchiveParser {
|
||||
private val PATTERN_ARCHIVE_URL =
|
||||
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
private val PATTERN_CURRENT_FUNDS =
|
||||
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_FUNDS =
|
||||
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
|
||||
private val PATTERN_HATH_FORM =
|
||||
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
private val PATTERN_HATH_ARCHIVE =
|
||||
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
|
||||
private val PATTERN_NEED_HATH_CLIENT =
|
||||
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
|
||||
fun parse(body: String): Result? {
|
||||
var m = PATTERN_HATH_FORM.matcher(body)
|
||||
if (!m.find()) {
|
||||
return null
|
||||
}
|
||||
val paramOr = m.group(1)
|
||||
val archiveList = ArrayList<Archive>()
|
||||
val d = Jsoup.parse(body)
|
||||
val es = d.select("#db>div>div")
|
||||
for (e in es) {
|
||||
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
|
||||
try {
|
||||
val res = e.selectFirst("form>input")!!.attr("value")
|
||||
val name = e.selectFirst("form>div>input")!!.attr("value")
|
||||
val size = e.selectFirst("p>strong")!!.text()
|
||||
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
|
||||
val item = Archive(res, name, size, cost, false)
|
||||
archiveList.add(item)
|
||||
} catch (ex: NullPointerException) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
m = PATTERN_HATH_ARCHIVE.matcher(body)
|
||||
while (m.find()) {
|
||||
val res = ParserUtils.trim(m.group(1))
|
||||
val name = ParserUtils.trim(m.group(2))
|
||||
val size = ParserUtils.trim(m.group(3))
|
||||
val cost = ParserUtils.trim(m.group(4))
|
||||
val item = Archive(res, name, size, cost, true)
|
||||
archiveList.add(item)
|
||||
}
|
||||
var funds: Funds? = null
|
||||
m = PATTERN_CURRENT_FUNDS.matcher(body)
|
||||
if (m.find()) {
|
||||
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(m.group(2), 0)
|
||||
funds = Funds(fundsGP, fundsC)
|
||||
}
|
||||
return Result(paramOr, archiveList, funds)
|
||||
}
|
||||
|
||||
@Throws(NoHAtHClientException::class)
|
||||
fun parseArchiveUrl(body: String): String? {
|
||||
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
|
||||
if (m.find()) {
|
||||
throw NoHAtHClientException("No H@H client")
|
||||
}
|
||||
val d = Jsoup.parse(body)
|
||||
val a = d.selectFirst("#continue>a[href]")
|
||||
return if (a != null) {
|
||||
a.attr("href") + "?start=1"
|
||||
} else null
|
||||
|
||||
// TODO: Check more errors
|
||||
}
|
||||
|
||||
fun parseFunds(body: String): Funds? {
|
||||
val m = PATTERN_FUNDS.matcher(body)
|
||||
return if (m.find()) {
|
||||
val fundsC = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
|
||||
return Funds(fundsGP, fundsC)
|
||||
} else null
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean
|
||||
) {
|
||||
fun format(getString: IntFunction<String?>): String {
|
||||
return if (isHAtH) {
|
||||
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
|
||||
"[H@H] $name [$size] [$costStr]"
|
||||
} else {
|
||||
val nameStr =
|
||||
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
|
||||
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
|
||||
"$nameStr [$size] [$costStr]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Funds(var fundsGP: Int, var fundsC: Int)
|
||||
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
@@ -38,7 +37,6 @@ import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -56,7 +54,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
public static PVLock pv = new PVLock(0);
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final LinkedHashMap<Integer, ByteBuffer> streams = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<Integer, Image.ByteBufferSource> streams = new LinkedHashMap<>();
|
||||
private final Thread[] decodeThread = new Thread[]{
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
@@ -305,10 +303,10 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
|
||||
Image.ByteBufferSource src = archiveAccessor.getImageSource(index);
|
||||
|
||||
synchronized (streams) {
|
||||
streams.put(index, buffer);
|
||||
streams.put(index, src);
|
||||
streams.notify();
|
||||
}
|
||||
}
|
||||
@@ -320,7 +318,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
ByteBuffer buffer;
|
||||
Image.ByteBufferSource src;
|
||||
synchronized (streams) {
|
||||
if (streams.isEmpty()) {
|
||||
try {
|
||||
@@ -331,21 +329,16 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, ByteBuffer> entry = iterator.next();
|
||||
Iterator<Map.Entry<Integer, Image.ByteBufferSource>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, Image.ByteBufferSource> entry = iterator.next();
|
||||
iterator.remove();
|
||||
index = entry.getKey();
|
||||
buffer = entry.getValue();
|
||||
src = entry.getValue();
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if (buffer != null) {
|
||||
try {
|
||||
image = Image.decode(buffer.asReadOnlyBuffer());
|
||||
} catch (ImageDecoder.DecodeException e) {
|
||||
UriArchiveAccessor.releaseByteBuffer(buffer);
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (src != null) {
|
||||
image = Image.decode(src);
|
||||
}
|
||||
if (image != null) {
|
||||
notifyPageSucceed(index, image);
|
||||
|
||||
@@ -97,7 +97,7 @@ class RestoreDownloadPreference constructor(
|
||||
runBlocking {
|
||||
fillGalleryListByApi(
|
||||
ArrayList(restoreItemList),
|
||||
EhUrl.getReferer()
|
||||
EhUrl.referer
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -17,22 +17,24 @@
|
||||
package com.hippo.ehviewer.spider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
import com.hippo.Native;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.gallery.PageLoader2;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.io.UniFileInputStreamPipe;
|
||||
import com.hippo.io.UniFileOutputStreamPipe;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.unifile.RawFile;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
@@ -40,14 +42,24 @@ import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.Utilities;
|
||||
|
||||
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.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Locale;
|
||||
|
||||
import coil.disk.DiskCache;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public final class SpiderDen {
|
||||
|
||||
@Nullable
|
||||
private static SimpleDiskCache sCache;
|
||||
private static DiskCache sCache; // We use data to store image file, and metadata for image type
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
private final long mGid;
|
||||
@Nullable
|
||||
@@ -61,8 +73,8 @@ public final class SpiderDen {
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sCache = new SimpleDiskCache(new File(context.getCacheDir(), "image"),
|
||||
MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024);
|
||||
sCache = new DiskCache.Builder().directory(new File(context.getCacheDir(), "gallery_image"))
|
||||
.maxSizeBytes((long) MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024).build();
|
||||
}
|
||||
|
||||
public static UniFile getGalleryDownloadDir(long gid) {
|
||||
@@ -137,7 +149,10 @@ public final class SpiderDen {
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.contain(key);
|
||||
var snapshot = sCache.get(key);
|
||||
var exist = snapshot != null;
|
||||
if (exist) snapshot.close();
|
||||
return exist;
|
||||
}
|
||||
|
||||
private boolean containInDownloadDir(int index) {
|
||||
@@ -171,42 +186,27 @@ public final class SpiderDen {
|
||||
}
|
||||
// Find image file in cache
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
InputStreamPipe pipe = sCache.getInputStreamPipe(key);
|
||||
if (pipe == null) {
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) {
|
||||
// Get extension
|
||||
String extension;
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
pipe.obtain();
|
||||
BitmapFactory.decodeStream(pipe.open(), null, options);
|
||||
pipe.close();
|
||||
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(options.outMimeType);
|
||||
if (extension != null) {
|
||||
extension = '.' + extension;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// Fix extension
|
||||
extension = fixExtension(extension);
|
||||
String extension = fixExtension("." + buf.readUtf8Line());
|
||||
// Copy from cache to download dir
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
os = file.openOutputStream();
|
||||
IOUtils.copy(pipe.open(), os);
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,13 +253,40 @@ public final class SpiderDen {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OutputStreamPipe openCacheOutputStreamPipe(int index) {
|
||||
private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.getOutputStreamPipe(key);
|
||||
var editor = sCache.edit(key);
|
||||
if (editor == null) return null;
|
||||
try (var sink = Okio.buffer(Okio.sink(editor.getMetadata().toFile()))) {
|
||||
sink.writeUtf8(extension);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new OutputStreamPipe() {
|
||||
private OutputStream os = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public OutputStream open() throws IOException {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
os = new FileOutputStream(editor.getData().toFile());
|
||||
return os;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,7 +314,7 @@ public final class SpiderDen {
|
||||
// Return the download pipe is the gallery has been downloaded
|
||||
OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension);
|
||||
if (pipe == null) {
|
||||
pipe = openCacheOutputStreamPipe(index);
|
||||
pipe = openCacheOutputStreamPipe(index, extension);
|
||||
}
|
||||
return pipe;
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
@@ -304,7 +331,29 @@ public final class SpiderDen {
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.getInputStreamPipe(key);
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) return null;
|
||||
return new InputStreamPipe() {
|
||||
private InputStream is = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
snapshot.close();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InputStream open() throws IOException {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
is = new FileInputStream(snapshot.getData().toFile());
|
||||
return is;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -341,4 +390,94 @@ public final class SpiderDen {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Image.ByteBufferSource getImageSource(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
if (sCache != null) {
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot != null) {
|
||||
try {
|
||||
var file = new RandomAccessFile(snapshot.getData().toFile(), "rw");
|
||||
var chan = file.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file.close();
|
||||
snapshot.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
UniFile file = findImageFile(dir, index);
|
||||
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
if (copyFromCacheToDownloadDir(index)) {
|
||||
file = findImageFile(dir, index);
|
||||
}
|
||||
}
|
||||
if (file instanceof RawFile rawFile) {
|
||||
try {
|
||||
var file2 = new RandomAccessFile(rawFile.mFile, "rw");
|
||||
var chan = file2.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file2.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file2.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
try {
|
||||
var pfd = EhApplication.getApplication().getContentResolver().openFileDescriptor(file.getUri(), "rw");
|
||||
var map = Native.mapFd(pfd.getFd(), pfd.getStatSize());
|
||||
if (map != null) {
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
Native.unmapDirectByteBuffer(map);
|
||||
pfd.close();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
pfd.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package com.hippo.ehviewer.spider;
|
||||
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
@@ -46,7 +45,6 @@ import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageApiParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||
import com.hippo.ehviewer.gallery.PageLoader2;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
@@ -57,7 +55,6 @@ import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
import com.hippo.yorozuya.StringUtils;
|
||||
import com.hippo.yorozuya.Utilities;
|
||||
import com.hippo.yorozuya.collect.SparseJLArray;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
import com.hippo.yorozuya.thread.PriorityThreadFactory;
|
||||
@@ -103,7 +100,7 @@ public final class SpiderQueen implements Runnable {
|
||||
public static final String SPIDER_INFO_FILENAME = ".ehviewer";
|
||||
private static final String TAG = SpiderQueen.class.getSimpleName();
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
private static final boolean DEBUG_LOG = false;
|
||||
private static final boolean DEBUG_LOG = true;
|
||||
private static final boolean DEBUG_PTOKEN = true;
|
||||
private static final String[] URL_509_SUFFIX_ARRAY = {
|
||||
"/509.gif",
|
||||
@@ -156,6 +153,8 @@ public final class SpiderQueen implements Runnable {
|
||||
// For download, when it go to mPageStateArray.size(), done
|
||||
private volatile int mDownloadPage = -1;
|
||||
|
||||
private boolean mStoped = false;
|
||||
|
||||
private SpiderQueen(@NonNull GalleryInfo galleryInfo) {
|
||||
mHttpClient = EhApplication.getOkHttpClient();
|
||||
mSpiderInfoCache = EhApplication.getSpiderInfoCache();
|
||||
@@ -388,10 +387,15 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
Thread queenThread = mQueenThread;
|
||||
if (queenThread != null) {
|
||||
queenThread.interrupt();
|
||||
mQueenThread = null;
|
||||
mStoped = true;
|
||||
synchronized (mQueenLock) {
|
||||
mQueenLock.notifyAll();
|
||||
}
|
||||
synchronized (mDecodeRequestQueue) {
|
||||
mDecodeRequestQueue.notifyAll();
|
||||
}
|
||||
synchronized (mWorkerLock) {
|
||||
mWorkerLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,7 +583,6 @@ public final class SpiderQueen implements Runnable {
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = file.openOutputStream();
|
||||
pipe.obtain();
|
||||
IOUtils.copy(pipe.open(), os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
@@ -605,8 +608,6 @@ public final class SpiderQueen implements Runnable {
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
pipe.obtain();
|
||||
|
||||
// Get dst file
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
@@ -645,8 +646,6 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
try {
|
||||
pipe.obtain();
|
||||
|
||||
// Get dst file
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
@@ -713,7 +712,7 @@ public final class SpiderQueen implements Runnable {
|
||||
return spiderInfo;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -868,6 +867,7 @@ public final class SpiderQueen implements Runnable {
|
||||
editor.commit();
|
||||
} catch (IOException e) {
|
||||
editor.abort();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,8 +875,8 @@ public final class SpiderQueen implements Runnable {
|
||||
// Read spider info
|
||||
SpiderInfo spiderInfo = readSpiderInfoFromLocal();
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -891,16 +891,16 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
mSpiderInfo.lazySet(spiderInfo);
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Write spider info to file
|
||||
writeSpiderInfoToLocal(spiderInfo);
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -924,17 +924,19 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
// handle pToken request
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
while (!mStoped) {
|
||||
Integer index = mRequestPTokenQueue.poll();
|
||||
|
||||
if (index == null) {
|
||||
// No request index, wait here
|
||||
synchronized (mQueenLock) {
|
||||
if (mStoped) break;
|
||||
try {
|
||||
mQueenLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
if (mStoped) break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -989,16 +991,9 @@ public final class SpiderQueen implements Runnable {
|
||||
// Set mQueenThread null
|
||||
mQueenThread = null;
|
||||
|
||||
// Interrupt decoder
|
||||
for (Thread decoderThread : mDecodeThreadArray) {
|
||||
if (decoderThread != null) {
|
||||
decoderThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Interrupt all workers
|
||||
// Stop all workers
|
||||
synchronized (mWorkerLock) {
|
||||
mWorkerPoolExecutor.shutdownNow();
|
||||
mWorkerPoolExecutor.shutdown();
|
||||
mWorkerPoolExecutor = null;
|
||||
}
|
||||
notifyFinish();
|
||||
@@ -1146,7 +1141,6 @@ public final class SpiderQueen implements Runnable {
|
||||
String pageUrl = null;
|
||||
String error = null;
|
||||
boolean forceHtml = false;
|
||||
boolean interrupt = false;
|
||||
boolean leakSkipHathKey = false;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
@@ -1191,10 +1185,9 @@ public final class SpiderQueen implements Runnable {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
error = "Interrupted";
|
||||
interrupt = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1226,10 +1219,9 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
error = "Interrupted";
|
||||
interrupt = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1252,7 +1244,6 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
// Download image
|
||||
InputStream is = null;
|
||||
try {
|
||||
if (DEBUG_LOG) {
|
||||
Log.d(TAG, "Start download image " + index);
|
||||
@@ -1276,10 +1267,6 @@ public final class SpiderQueen implements Runnable {
|
||||
if (mediaType != null) {
|
||||
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType.toString());
|
||||
}
|
||||
// Ensure extension
|
||||
if (!Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) {
|
||||
extension = PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0];
|
||||
}
|
||||
|
||||
OutputStreamPipe osPipe = null;
|
||||
try {
|
||||
@@ -1293,29 +1280,43 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
long contentLength = responseBody.contentLength();
|
||||
is = responseBody.byteStream();
|
||||
osPipe.obtain();
|
||||
OutputStream os = osPipe.open();
|
||||
|
||||
final byte[] data = new byte[1024 * 4];
|
||||
long receivedSize = 0;
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int bytesRead = is.read(data);
|
||||
if (bytesRead == -1) {
|
||||
response.close();
|
||||
break;
|
||||
OutputStream os = osPipe.open();
|
||||
if (os instanceof FileOutputStream fileOutputStream) {
|
||||
try (var channel = fileOutputStream.getChannel(); var source = responseBody.source(); responseBody; response) {
|
||||
while (!mStoped) {
|
||||
// Is 40k a good size ?
|
||||
long bytesRead = channel.transferFrom(source, receivedSize, 40960);
|
||||
if (bytesRead == 0) {
|
||||
break;
|
||||
}
|
||||
receivedSize += bytesRead;
|
||||
if (contentLength > 0) {
|
||||
mPagePercentMap.put(index, (float) receivedSize / contentLength);
|
||||
}
|
||||
notifyPageDownload(index, contentLength, receivedSize, (int) bytesRead);
|
||||
}
|
||||
}
|
||||
os.write(data, 0, bytesRead);
|
||||
receivedSize += bytesRead;
|
||||
// Update page percent
|
||||
if (contentLength > 0) {
|
||||
mPagePercentMap.put(index, (float) receivedSize / contentLength);
|
||||
} else {
|
||||
final byte[] data = new byte[1024 * 4];
|
||||
try (var is = responseBody.byteStream(); responseBody; response) {
|
||||
while (!mStoped) {
|
||||
int bytesRead = is.read(data);
|
||||
if (bytesRead == -1) {
|
||||
break;
|
||||
}
|
||||
os.write(data, 0, bytesRead);
|
||||
receivedSize += bytesRead;
|
||||
// Update page percent
|
||||
if (contentLength > 0) {
|
||||
mPagePercentMap.put(index, (float) receivedSize / contentLength);
|
||||
}
|
||||
// Notify listener
|
||||
notifyPageDownload(index, contentLength, receivedSize, bytesRead);
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
// Notify listener
|
||||
notifyPageDownload(index, contentLength, receivedSize, bytesRead);
|
||||
}
|
||||
os.flush();
|
||||
|
||||
// check download size
|
||||
if (contentLength >= 0) {
|
||||
@@ -1346,7 +1347,6 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
|
||||
// Check plain txt
|
||||
isPipe.obtain();
|
||||
InputStream inputStream = new BufferedInputStream(isPipe.open());
|
||||
boolean isPlainTxt = true;
|
||||
int j = 0;
|
||||
@@ -1377,9 +1377,8 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
// Check interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
interrupt = true;
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
error = "Interrupted";
|
||||
break;
|
||||
}
|
||||
@@ -1395,16 +1394,12 @@ public final class SpiderQueen implements Runnable {
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Thread.sleep(0); // to make allow to catch InterruptedException
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
error = GetText.getString(R.string.error_socket);
|
||||
forceHtml = true;
|
||||
} catch (InterruptedException ignored) {
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
Log.d(TAG, "End download image " + index);
|
||||
}
|
||||
@@ -1415,7 +1410,7 @@ public final class SpiderQueen implements Runnable {
|
||||
mSpiderDen.remove(index);
|
||||
|
||||
updatePageState(index, STATE_FAILED, error);
|
||||
return !interrupt;
|
||||
return !mStoped;
|
||||
}
|
||||
|
||||
// false for stop
|
||||
@@ -1486,7 +1481,7 @@ public final class SpiderQueen implements Runnable {
|
||||
|
||||
String pToken = null;
|
||||
// Get token
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
while (!mStoped) {
|
||||
synchronized (mPTokenLock) {
|
||||
pToken = spiderInfo.pTokenMap.get(index);
|
||||
}
|
||||
@@ -1498,6 +1493,7 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
// Wait
|
||||
synchronized (mWorkerLock) {
|
||||
if (mStoped) break;
|
||||
try {
|
||||
mWorkerLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
@@ -1507,6 +1503,7 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mStoped) break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
@@ -1523,7 +1520,7 @@ public final class SpiderQueen implements Runnable {
|
||||
String previousPToken = null;
|
||||
int previousIndex = index - 1;
|
||||
// Get token
|
||||
while (previousIndex >= 0 && !Thread.currentThread().isInterrupted()) {
|
||||
while (previousIndex >= 0 && !mStoped) {
|
||||
synchronized (mPTokenLock) {
|
||||
previousPToken = spiderInfo.pTokenMap.get(previousIndex);
|
||||
}
|
||||
@@ -1535,6 +1532,7 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
// Wait
|
||||
synchronized (mWorkerLock) {
|
||||
if (mStoped) break;
|
||||
try {
|
||||
mWorkerLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
@@ -1544,6 +1542,7 @@ public final class SpiderQueen implements Runnable {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mStoped) break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
@@ -1567,8 +1566,7 @@ public final class SpiderQueen implements Runnable {
|
||||
Log.i(TAG, Thread.currentThread().getName() + ": start");
|
||||
}
|
||||
|
||||
while (mSpiderDen.isReady() && !Thread.currentThread().isInterrupted() && runInternal())
|
||||
;
|
||||
while (mSpiderDen.isReady() && !mStoped && runInternal()) ;
|
||||
|
||||
boolean finish;
|
||||
// Clear in spider worker array
|
||||
@@ -1611,9 +1609,10 @@ public final class SpiderQueen implements Runnable {
|
||||
Log.i(TAG, Thread.currentThread().getName() + ": start");
|
||||
}
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
while (!mStoped) {
|
||||
int index;
|
||||
synchronized (mDecodeRequestQueue) {
|
||||
if (mStoped) break;
|
||||
if (mDecodeRequestQueue.isEmpty()) {
|
||||
try {
|
||||
mDecodeRequestQueue.wait();
|
||||
@@ -1621,6 +1620,7 @@ public final class SpiderQueen implements Runnable {
|
||||
// Interrupted
|
||||
break;
|
||||
}
|
||||
if (mStoped) break;
|
||||
continue;
|
||||
}
|
||||
index = mDecodeRequestQueue.remove();
|
||||
@@ -1634,8 +1634,8 @@ public final class SpiderQueen implements Runnable {
|
||||
continue;
|
||||
}
|
||||
|
||||
InputStreamPipe pipe = mSpiderDen.openInputStreamPipe(index);
|
||||
if (pipe == null) {
|
||||
Image.ByteBufferSource src = mSpiderDen.getImageSource(index);
|
||||
if (src == null) {
|
||||
resetDecodeIndex();
|
||||
// Can't find the file, it might be removed from cache,
|
||||
// Reset it state and request it
|
||||
@@ -1644,36 +1644,11 @@ public final class SpiderQueen implements Runnable {
|
||||
continue;
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
Image image = Image.decode(src);
|
||||
String error = null;
|
||||
InputStream is;
|
||||
|
||||
pipe.obtain();
|
||||
try {
|
||||
is = pipe.open();
|
||||
} catch (IOException e) {
|
||||
// Can't open pipe
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
is = null;
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
|
||||
if (is != null) {
|
||||
try {
|
||||
image = Image.decode(((FileInputStream) is));
|
||||
} catch (ImageDecoder.DecodeException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (image == null) {
|
||||
error = GetText.getString(R.string.error_decoding_failed);
|
||||
}
|
||||
if (image == null) {
|
||||
error = GetText.getString(R.string.error_decoding_failed);
|
||||
}
|
||||
|
||||
// Notify
|
||||
|
||||
@@ -24,15 +24,20 @@ import android.content.pm.verify.domain.DomainVerificationManager
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState
|
||||
import android.graphics.Bitmap
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.customview.widget.Openable
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
@@ -58,6 +63,7 @@ import com.hippo.ehviewer.client.parser.GalleryListUrlParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||
import com.hippo.ehviewer.databinding.ActivityMainBinding
|
||||
import com.hippo.ehviewer.download.DownloadService
|
||||
import com.hippo.ehviewer.ui.login.LoginActivity
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene
|
||||
@@ -82,6 +88,8 @@ import java.io.OutputStream
|
||||
class MainActivity : EhActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var connectivityManager: ConnectivityManager
|
||||
private val availableNetworks: MutableList<Network> = mutableListOf()
|
||||
|
||||
private fun saveImageToTempFile(file: UniFile?): File? {
|
||||
file ?: return null
|
||||
@@ -216,6 +224,10 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
connectivityManager = getSystemService()!!
|
||||
if (Settings.getDF() && Settings.getBypassVPN()) {
|
||||
bypassVPN()
|
||||
}
|
||||
if (EhUtils.needSignedIn())
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -275,6 +287,19 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun bypassVPN() {
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
capabilities?.let {
|
||||
if (it.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
val builder = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
connectivityManager.registerNetworkCallback(builder.build(), mNetworkCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
private fun checkAppLinkVerify() {
|
||||
@@ -333,8 +358,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
|
||||
private fun checkMeteredNetwork() {
|
||||
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
if (cm.isActiveNetworkMetered) {
|
||||
if (connectivityManager.isActiveNetworkMetered) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Snackbar.make(
|
||||
binding.drawView,
|
||||
@@ -502,4 +526,25 @@ class MainActivity : EhActivity() {
|
||||
override fun onDrawerClosed(drawerView: View) {}
|
||||
override fun onDrawerStateChanged(newState: Int) {}
|
||||
}
|
||||
|
||||
private val mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
private val TAG = "mNetworkCallback"
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d(TAG, "onAvailable: $network")
|
||||
connectivityManager.bindProcessToNetwork(network)
|
||||
availableNetworks.add(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "onLost: $network")
|
||||
val activeNetwork = availableNetworks.last()
|
||||
availableNetworks.remove(network)
|
||||
if (network == activeNetwork) {
|
||||
connectivityManager.bindProcessToNetwork(
|
||||
availableNetworks.takeIf { it.isNotEmpty() }?.last()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,6 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
addPreferencesFromResource(R.xml.advanced_settings)
|
||||
val dumpLogcat = findPreference<Preference>(KEY_DUMP_LOGCAT)
|
||||
val appLanguage = findPreference<Preference>(KEY_APP_LANGUAGE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) appLanguage!!.isVisible = false
|
||||
val importData = findPreference<Preference>(KEY_IMPORT_DATA)
|
||||
val exportData = findPreference<Preference>(KEY_EXPORT_DATA)
|
||||
val backupFavorite = findPreference<Preference>(KEY_BACKUP_FAVORITE)
|
||||
@@ -378,7 +377,7 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val key = preference.key
|
||||
if (KEY_APP_LANGUAGE == key && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
if (KEY_APP_LANGUAGE == key) {
|
||||
if ("system" == newValue) {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,9 @@ import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
|
||||
class EhFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -67,6 +69,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
return true
|
||||
} else if (Settings.KEY_GALLERY_SITE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
launchIO { runCatching { EhEngine.getUConfig() } }
|
||||
return true
|
||||
} else if (Settings.KEY_LIST_MODE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -53,7 +53,6 @@ import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
+8
-14
@@ -1,19 +1,15 @@
|
||||
package com.hippo.ehviewer.ui
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.ui.compose.CookieSignInScene
|
||||
import com.hippo.ehviewer.ui.compose.SelectSiteScreen
|
||||
import com.hippo.ehviewer.ui.compose.SignInScreen
|
||||
import com.hippo.ehviewer.ui.compose.WebviewSignInScreen
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
|
||||
class LoginActivity : ComponentActivity() {
|
||||
class LoginActivity : EhActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
@@ -47,11 +43,9 @@ class LoginActivity : ComponentActivity() {
|
||||
super.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SIGN_IN_ROUTE_NAME = "SignIn"
|
||||
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
|
||||
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
|
||||
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
|
||||
}
|
||||
}
|
||||
|
||||
const val SIGN_IN_ROUTE_NAME = "SignIn"
|
||||
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
|
||||
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
|
||||
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
@@ -138,5 +138,6 @@ suspend fun getProfile() {
|
||||
Settings.putDisplayName(displayName)
|
||||
Settings.putAvatar(avatar)
|
||||
}
|
||||
EhEngine.getUConfig()
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -21,10 +21,10 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -53,14 +53,10 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.UrlOpener
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.COOKIE_SIGN_IN_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.WEBVIEW_SIGN_IN_ROUTE_NAME
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -192,7 +188,7 @@ fun SignInScreen(navController: NavController) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row {
|
||||
OutlinedButton(
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
UrlOpener.openUrl(
|
||||
requireActivity(context),
|
||||
+4
-5
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
@@ -14,7 +14,6 @@ import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.ehviewer.widget.DialogWebChromeClient
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
@@ -69,9 +68,9 @@ fun WebviewSignInScreen(navController: NavController) {
|
||||
EhCookieStore.newCookie(
|
||||
cookie,
|
||||
domain,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
forcePersistent = true,
|
||||
forceLongLive = true,
|
||||
forceNotHostOnly = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1029,8 +1029,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
return
|
||||
}
|
||||
val info = mList!![position]
|
||||
if (info.thumb != null) {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb!!)
|
||||
info.thumb?.let {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), EhUtils.fixThumbUrl(it))
|
||||
}
|
||||
holder.title.text = EhUtils.getSuitableTitle(info)
|
||||
holder.uploader.text = info.uploader
|
||||
|
||||
@@ -941,7 +941,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
mModifyGiList.clear()
|
||||
val url: String = if (local) {
|
||||
// Local fav is shown now, but operation need be done for cloud fav
|
||||
EhUrl.getFavoritesUrl()
|
||||
EhUrl.favoritesUrl
|
||||
} else {
|
||||
mUrlBuilder!!.build()
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.widget.TileThumb
|
||||
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
|
||||
@@ -169,6 +170,9 @@ internal abstract class GalleryAdapter(
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
|
||||
val gi = getDataAt(position) ?: return
|
||||
if (gi is LocalFavoriteInfo) {
|
||||
gi.thumb = EhUtils.fixThumbUrl(gi.thumb!!)
|
||||
}
|
||||
when (mType) {
|
||||
TYPE_LIST -> {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
|
||||
|
||||
@@ -78,6 +78,7 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
import com.hippo.ehviewer.client.parser.ParserUtils
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
@@ -148,6 +149,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
private var mArchiveFormParamOr: String? = null
|
||||
private var mArchiveList: List<ArchiveParser.Archive>? = null
|
||||
private var mCurrentFunds: ArchiveParser.Funds? = null
|
||||
|
||||
@State
|
||||
private var mState = STATE_INIT
|
||||
@@ -393,7 +395,8 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
} else {
|
||||
binding.content.comments.comments.visibility = View.GONE
|
||||
}
|
||||
binding.content.previews.previews.setOnClickListener(this)
|
||||
binding.content.previews.previews.setOnClickListener { navigateToPreview() }
|
||||
binding.content.previews.previewText.setOnClickListener { navigateToPreview(true) }
|
||||
mViewTransition2 = ViewTransition(binding.content.belowHeader, binding.content.progress)
|
||||
if (prepareData()) {
|
||||
if (mGalleryDetail != null) {
|
||||
@@ -690,7 +693,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
private fun bindPreviews(gd: GalleryDetail) {
|
||||
val inflater = layoutInflater
|
||||
resourcesOrNull ?: return
|
||||
val previewNum = Settings.getPreviewNum()
|
||||
_binding ?: return
|
||||
binding.content.previews.run {
|
||||
gridLayout.removeAllViews()
|
||||
@@ -698,7 +700,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (gd.previewPages <= 0 || previewSet == null || previewSet.size() == 0) {
|
||||
previewText.setText(R.string.no_previews)
|
||||
return
|
||||
} else if (gd.previewPages == 1 && previewSet.size() <= previewNum) {
|
||||
} else if (gd.previewPages == 1) {
|
||||
previewText.setText(R.string.no_more_previews)
|
||||
} else {
|
||||
previewText.setText(R.string.more_previews)
|
||||
@@ -706,16 +708,19 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val columnWidth = Settings.getThumbSize()
|
||||
gridLayout.setColumnSize(columnWidth)
|
||||
gridLayout.setStrategy(SimpleGridAutoSpanLayout.STRATEGY_SUITABLE_SIZE)
|
||||
val size = previewNum.coerceAtMost(previewSet.size())
|
||||
for (i in 0 until size) {
|
||||
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
|
||||
gridLayout.addView(view)
|
||||
val image = view.findViewById<LoadImageView>(R.id.image)
|
||||
previewSet.load(image, gd.gid, i)
|
||||
image.setTag(R.id.index, i)
|
||||
image.setOnClickListener(this@GalleryDetailScene)
|
||||
val text = view.findViewById<TextView>(R.id.text)
|
||||
text.text = (previewSet.getPosition(i) + 1).toString()
|
||||
viewLifecycleOwner.lifecycleScope.launchIO {
|
||||
for (i in 0 until previewSet.size()) {
|
||||
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
|
||||
val image = view.findViewById<LoadImageView>(R.id.image)
|
||||
previewSet.load(image, gd.gid, i)
|
||||
image.setTag(R.id.index, i)
|
||||
withUIContext {
|
||||
gridLayout.addView(view)
|
||||
image.setOnClickListener(this@GalleryDetailScene)
|
||||
val text = view.findViewById<TextView>(R.id.text)
|
||||
text.text = (previewSet.getPosition(i) + 1).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -774,6 +779,15 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToPreview(nextPage: Boolean = false) {
|
||||
mGalleryDetail?.let {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, it)
|
||||
args.putBoolean(GalleryPreviewsScene.KEY_NEXT_PAGE, nextPage)
|
||||
navigate(R.id.galleryPreviewsScene, args)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val context = context
|
||||
val activity = mainActivity
|
||||
@@ -961,12 +975,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
args.putParcelable(GalleryCommentsScene.KEY_COMMENT_LIST, mGalleryDetail!!.comments)
|
||||
args.putParcelable(GalleryCommentsScene.KEY_GALLERY_DETAIL, mGalleryDetail)
|
||||
navigate(R.id.galleryCommentsScene, args)
|
||||
} else if (binding.content.previews.previews === v) {
|
||||
if (null != mGalleryDetail) {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, mGalleryDetail)
|
||||
navigate(R.id.galleryPreviewsScene, args)
|
||||
}
|
||||
} else {
|
||||
var o = v.getTag(R.id.tag)
|
||||
if (o is String) {
|
||||
@@ -1432,12 +1440,12 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
.setCallback(this)
|
||||
mRequest!!.enqueue(this@GalleryDetailScene)
|
||||
} else {
|
||||
bind(mArchiveList)
|
||||
bind(mArchiveList, mCurrentFunds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bind(data: List<ArchiveParser.Archive>?) {
|
||||
private fun bind(data: List<ArchiveParser.Archive>?, funds: ArchiveParser.Funds?) {
|
||||
if (null == mDialog || null == mProgressView || null == mErrorText || null == mListView) {
|
||||
return
|
||||
}
|
||||
@@ -1457,6 +1465,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
mListView!!.visibility = View.VISIBLE
|
||||
mListView!!.adapter =
|
||||
ArrayAdapter(mDialog!!.context, R.layout.item_select_dialog, nameArray)
|
||||
if (funds != null) {
|
||||
var fundsGP = funds.fundsGP.toString()
|
||||
// Ex GP numbers are rounded down to the nearest thousand
|
||||
if (Settings.getGallerySite() == EhUrl.SITE_EX) {
|
||||
fundsGP += "+"
|
||||
}
|
||||
mDialog!!.setTitle(getString(R.string.current_funds, fundsGP, funds.fundsC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1464,7 +1480,23 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val context = context
|
||||
val activity = mainActivity
|
||||
if (null != context && null != activity && null != mArchiveList && position < mArchiveList!!.size) {
|
||||
val res = mArchiveList!![position].res()
|
||||
if (mArchiveList!![position].name == "Insufficient Funds") {
|
||||
showTip(R.string.insufficient_funds, LENGTH_SHORT)
|
||||
return
|
||||
} else if (mCurrentFunds != null) {
|
||||
val cost = ParserUtils.parseInt(
|
||||
mArchiveList!![position].cost.removeSuffix("GP").removeSuffix("Credits"), 0
|
||||
)
|
||||
if (cost <= mCurrentFunds!!.fundsGP) {
|
||||
mCurrentFunds!!.fundsGP -= cost
|
||||
} else if (cost <= mCurrentFunds!!.fundsC) {
|
||||
mCurrentFunds!!.fundsC -= cost
|
||||
} else {
|
||||
showTip(R.string.insufficient_funds, LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
}
|
||||
val res = mArchiveList!![position].res
|
||||
val isHAtH = mArchiveList!![position].isHAtH
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_DOWNLOAD_ARCHIVE)
|
||||
@@ -1498,9 +1530,10 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
override fun onSuccess(result: ArchiveParser.Result) {
|
||||
if (mRequest != null) {
|
||||
mRequest = null
|
||||
mArchiveFormParamOr = result.paramOr()
|
||||
mArchiveList = result.archiveList()
|
||||
bind(result.archiveList())
|
||||
mArchiveFormParamOr = result.paramOr
|
||||
mArchiveList = result.archiveList
|
||||
mCurrentFunds = result.funds
|
||||
bind(result.archiveList, result.funds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -401,12 +401,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
|
||||
mActionFabDrawable = AddDeleteDrawable(context, colorID)
|
||||
binding.fabLayout.primaryFab!!.setImageDrawable(mActionFabDrawable)
|
||||
mSearchFab!!.setOnClickListener {
|
||||
if (STATE_NORMAL != mState) {
|
||||
onApplySearch()
|
||||
hideSoftInput()
|
||||
}
|
||||
}
|
||||
mSearchFab!!.setOnClickListener { onApplySearch() }
|
||||
|
||||
// Update list url builder
|
||||
onUpdateUrlBuilder()
|
||||
@@ -701,7 +696,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
|
||||
override fun onSearchViewExpanded() {
|
||||
super.onSearchViewExpanded()
|
||||
selectSearchFab(true)
|
||||
if (mState == STATE_NORMAL) selectSearchFab(true)
|
||||
}
|
||||
|
||||
override fun onSearchViewHidden() {
|
||||
@@ -1279,11 +1274,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
return if (null != mHelper) mHelper!!.size() else 0
|
||||
}
|
||||
|
||||
public override fun onItemClick(view: View, position: Int) {
|
||||
override fun onItemClick(view: View, position: Int) {
|
||||
this@GalleryListScene.onItemClick(position)
|
||||
}
|
||||
|
||||
public override fun onItemLongClick(view: View, position: Int): Boolean {
|
||||
override fun onItemLongClick(view: View, position: Int): Boolean {
|
||||
return this@GalleryListScene.onItemLongClick(position)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
private var mAdapter: GalleryPreviewAdapter? = null
|
||||
private var mHelper: GalleryPreviewHelper? = null
|
||||
private var mHasFirstRefresh = false
|
||||
private var mNextPage: Boolean = false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
@@ -71,6 +72,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
private fun onInit() {
|
||||
val args = arguments ?: return
|
||||
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO)
|
||||
mNextPage = args.getBoolean(KEY_NEXT_PAGE)
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
@@ -137,6 +139,12 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
if (mGalleryInfo != null) {
|
||||
if ((mGalleryInfo as GalleryDetail).previewPages > 2) showMenu(R.menu.scene_gallery_previews)
|
||||
}
|
||||
runCatching {
|
||||
mHelper!!.mPages = (mGalleryInfo as GalleryDetail).previewPages
|
||||
if (mNextPage && mHelper!!.pages > 1) mHelper!!.goTo(1)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
@@ -344,6 +352,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
|
||||
companion object {
|
||||
const val KEY_GALLERY_INFO = "gallery_info"
|
||||
const val KEY_NEXT_PAGE = "next_page"
|
||||
private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh"
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,9 @@ class HistoryScene : BaseToolbarScene() {
|
||||
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
|
||||
val gi: GalleryInfo? = getItem(position)
|
||||
gi ?: return
|
||||
gi.thumb?.let { holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), it) }
|
||||
gi.thumb?.let {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), EhUtils.fixThumbUrl(it))
|
||||
}
|
||||
holder.title.text = EhUtils.getSuitableTitle(gi)
|
||||
holder.uploader.text = gi.uploader
|
||||
holder.rating.rating = gi.rating
|
||||
|
||||
@@ -73,7 +73,7 @@ class ProgressScene : BaseScene(), View.OnClickListener {
|
||||
context
|
||||
)
|
||||
)
|
||||
request.enqueue(this)
|
||||
request.enqueue()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -53,8 +53,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
updateSuggestions()
|
||||
}
|
||||
binding.searchview.editText.setOnEditorActionListener { _, _, _ ->
|
||||
binding.toolbar.text = binding.searchview.text
|
||||
binding.searchview.hide()
|
||||
onApplySearch()
|
||||
true
|
||||
}
|
||||
@@ -157,6 +155,8 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
}
|
||||
|
||||
fun onApplySearch() {
|
||||
binding.toolbar.text = binding.searchview.text
|
||||
binding.searchview.hide()
|
||||
val query = binding.toolbar.text.toString().trim()
|
||||
if (!mAllowEmptySearch && query.isEmpty()) {
|
||||
return
|
||||
|
||||
@@ -35,7 +35,6 @@ class AdvanceSearchTable @JvmOverloads constructor(
|
||||
) : LinearLayout(context, attrs) {
|
||||
private var mSh: CheckBox
|
||||
private var mSto: CheckBox
|
||||
private var mSr: CheckBox
|
||||
private var mMinRating: TextInputLayout
|
||||
private var mSp: CheckBox
|
||||
private var mSpf: EditText
|
||||
@@ -53,8 +52,7 @@ class AdvanceSearchTable @JvmOverloads constructor(
|
||||
mSh = row0.getChildAt(0) as CheckBox
|
||||
mSto = row0.getChildAt(1) as CheckBox
|
||||
val row1 = getChildAt(1) as ViewGroup
|
||||
mSr = row1.getChildAt(0) as CheckBox
|
||||
mMinRating = row1.getChildAt(1) as TextInputLayout
|
||||
mMinRating = row1.getChildAt(0) as TextInputLayout
|
||||
val row2 = getChildAt(2) as ViewGroup
|
||||
mSp = row2.getChildAt(0) as CheckBox
|
||||
mSpf = row2.getChildAt(1) as EditText
|
||||
@@ -72,11 +70,6 @@ class AdvanceSearchTable @JvmOverloads constructor(
|
||||
true
|
||||
}
|
||||
|
||||
mMinRating.isEnabled = mSr.isChecked
|
||||
mSr.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
|
||||
mMinRating.isEnabled = isChecked
|
||||
}
|
||||
|
||||
mSpt.isEnabled = mSp.isChecked
|
||||
mSpf.isEnabled = mSp.isChecked
|
||||
mSp.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
|
||||
@@ -105,22 +98,17 @@ class AdvanceSearchTable @JvmOverloads constructor(
|
||||
var minRating: Int
|
||||
get() {
|
||||
val position = mArray.indexOf(mMinRating.editText!!.text.toString())
|
||||
return if (mSr.isChecked && position >= 0) {
|
||||
position + 2
|
||||
return if (position > 0) {
|
||||
position + 1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
set(minRating) {
|
||||
if (minRating in 2..5) {
|
||||
mSr.isChecked = true
|
||||
(mMinRating.editText!! as AutoCompleteTextView).setText(
|
||||
mArray[minRating - 2],
|
||||
false
|
||||
)
|
||||
} else {
|
||||
mSr.isChecked = false
|
||||
}
|
||||
(mMinRating.editText!! as AutoCompleteTextView).setText(
|
||||
mArray[if (minRating in 2..5) minRating - 1 else 0],
|
||||
false
|
||||
)
|
||||
}
|
||||
var pageFrom: Int
|
||||
get() = if (mSp.isChecked) {
|
||||
|
||||
@@ -103,8 +103,8 @@ class SearchLayout @JvmOverloads constructor(
|
||||
mChip.isChecked = NumberUtils.int2boolean(mPair.first and mCategoryStored)
|
||||
mChip.setOnLongClickListener {
|
||||
if (mChip.isChecked) {
|
||||
for (i in 1 until mCategoryGroup.childCount + 1) {
|
||||
mCategoryGroup.check(i)
|
||||
for (i in 0 until mCategoryGroup.childCount) {
|
||||
(mCategoryGroup.getChildAt(i) as Chip).isChecked = true
|
||||
}
|
||||
mChip.isChecked = false
|
||||
} else {
|
||||
|
||||
@@ -19,57 +19,44 @@ package com.hippo.image
|
||||
|
||||
import android.graphics.ColorSpace
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.graphics.ImageDecoder.ImageInfo
|
||||
import android.graphics.ImageDecoder.Source
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import coil.decode.FrameDelayRewritingSource
|
||||
import com.hippo.Native
|
||||
import com.hippo.UriArchiveAccessor
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import kotlin.math.min
|
||||
|
||||
class Image private constructor(source: Source, private val byteBuffer: ByteBuffer? = null) {
|
||||
var mObtainedDrawable: Drawable?
|
||||
|
||||
init {
|
||||
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)
|
||||
)
|
||||
class Image private constructor(source: Source, private var src: ByteBufferSource?) {
|
||||
var mObtainedDrawable: Drawable? =
|
||||
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)
|
||||
)
|
||||
}.also {
|
||||
(it as? BitmapDrawable)?.run {
|
||||
src?.close()
|
||||
src = null
|
||||
}
|
||||
if (mObtainedDrawable is BitmapDrawable)
|
||||
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
|
||||
}
|
||||
}
|
||||
private set
|
||||
|
||||
val width: Int
|
||||
get() = mObtainedDrawable?.intrinsicWidth ?: 0
|
||||
val height: Int
|
||||
get() = mObtainedDrawable?.intrinsicHeight ?: 0
|
||||
val size: Int
|
||||
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
|
||||
|
||||
@Synchronized
|
||||
fun recycle() {
|
||||
(mObtainedDrawable as? Animatable)?.run {
|
||||
stop()
|
||||
byteBuffer?.let { UriArchiveAccessor.releaseByteBuffer(it) }
|
||||
}
|
||||
(mObtainedDrawable as? BitmapDrawable)?.run { bitmap.recycle() }
|
||||
(mObtainedDrawable as? Animatable)?.stop()
|
||||
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
|
||||
mObtainedDrawable?.callback = null
|
||||
mObtainedDrawable = null
|
||||
src?.close()
|
||||
src = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -83,44 +70,25 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff
|
||||
) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB
|
||||
)
|
||||
|
||||
@Throws(DecodeException::class)
|
||||
@JvmStatic
|
||||
fun decode(stream: FileInputStream): Image {
|
||||
val buffer = stream.channel.map(
|
||||
FileChannel.MapMode.READ_ONLY, 0,
|
||||
stream.available().toLong()
|
||||
)
|
||||
val source = if (checkIsGif(buffer)) {
|
||||
rewriteSource(stream.source().buffer())
|
||||
} else {
|
||||
buffer
|
||||
}
|
||||
val src = ImageDecoder.createSource(source)
|
||||
return Image(src)
|
||||
fun decode(src: ByteBufferSource): Image? {
|
||||
val directBuffer = src.getByteBuffer()
|
||||
check(directBuffer.isDirect)
|
||||
rewriteGifSource(directBuffer)
|
||||
return runCatching {
|
||||
Image(ImageDecoder.createSource(directBuffer), src)
|
||||
}.onFailure {
|
||||
src.close()
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@Throws(DecodeException::class)
|
||||
@JvmStatic
|
||||
fun decode(buffer: ByteBuffer): Image {
|
||||
return if (checkIsGif(buffer)) {
|
||||
val rewritten = rewriteSource(Buffer().apply {
|
||||
write(buffer)
|
||||
UriArchiveAccessor.releaseByteBuffer(buffer)
|
||||
})
|
||||
Image(ImageDecoder.createSource(rewritten))
|
||||
} else {
|
||||
Image(ImageDecoder.createSource(buffer), buffer)
|
||||
}
|
||||
}
|
||||
private external fun rewriteGifSource(buffer: ByteBuffer)
|
||||
}
|
||||
|
||||
private fun rewriteSource(source: BufferedSource): ByteBuffer {
|
||||
val bufferedSource = FrameDelayRewritingSource(source).buffer()
|
||||
return ByteBuffer.wrap(bufferedSource.use { it.readByteArray() })
|
||||
}
|
||||
|
||||
private fun checkIsGif(buffer: ByteBuffer): Boolean {
|
||||
check(buffer.isDirect)
|
||||
return Native.isGif(buffer)
|
||||
}
|
||||
interface ByteBufferSource : AutoCloseable {
|
||||
// A read-write direct bytebuffer, it may in native heap or file mapping
|
||||
fun getByteBuffer(): ByteBuffer
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,6 @@ public class UniFileInputStreamPipe implements InputStreamPipe {
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Empty
|
||||
|
||||
@@ -34,11 +34,6 @@ public class UniFileOutputStreamPipe implements OutputStreamPipe {
|
||||
mUniFile = uniFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Empty
|
||||
|
||||
@@ -21,32 +21,12 @@ import androidx.annotation.NonNull;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* A pipe to open {@code InputStream}
|
||||
*/
|
||||
public interface InputStreamPipe {
|
||||
|
||||
/**
|
||||
* Obtain the {@code InputStreamPipe}.
|
||||
* Call it before opening {@code InputStream}.
|
||||
*/
|
||||
void obtain();
|
||||
|
||||
/**
|
||||
* Release the {@code InputStreamPipe}.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Open {@code InputStream}
|
||||
*
|
||||
* @return the {@code InputStream}
|
||||
*/
|
||||
@NonNull
|
||||
InputStream open() throws IOException;
|
||||
|
||||
/**
|
||||
* Close the {@code InputStream} opened before.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
@@ -21,32 +21,12 @@ import androidx.annotation.NonNull;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* A pipe to open {@code OutputStream}
|
||||
*/
|
||||
public interface OutputStreamPipe {
|
||||
|
||||
/**
|
||||
* Obtain the {@code OutputStreamPipe}.
|
||||
* Call it before opening {@code OutputStream}.
|
||||
*/
|
||||
void obtain();
|
||||
|
||||
/**
|
||||
* Release the {@code OutputStreamPipe}.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Open {@code OutputStream}
|
||||
*
|
||||
* @return the {@code OutputStream}
|
||||
*/
|
||||
@NonNull
|
||||
OutputStream open() throws IOException;
|
||||
|
||||
/**
|
||||
* Close the {@code OutputStream} opened before.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
@@ -32,11 +32,11 @@ import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class RawFile extends UniFile {
|
||||
public class RawFile extends UniFile {
|
||||
|
||||
private static final String TAG = RawFile.class.getSimpleName();
|
||||
|
||||
private File mFile;
|
||||
public File mFile;
|
||||
|
||||
RawFile(UniFile parent, File file) {
|
||||
super(parent);
|
||||
|
||||
@@ -50,8 +50,6 @@ public final class BitmapUtils {
|
||||
public static Bitmap decodeStream(@NonNull InputStreamPipe isp, int maxWidth, int maxHeight,
|
||||
int pixels, boolean checkMemory, boolean justCalc, int[] sampleSize) {
|
||||
try {
|
||||
isp.obtain();
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(isp.open(), null, options);
|
||||
|
||||
@@ -227,7 +227,7 @@ public class ContentLayout extends FrameLayout {
|
||||
/**
|
||||
* The available page count.
|
||||
*/
|
||||
private int mPages;
|
||||
public int mPages;
|
||||
private int mNextPage;
|
||||
private int mCurrentTaskId;
|
||||
private int mCurrentTaskType;
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat
|
||||
import coil.imageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.hippo.drawable.PreciselyClipDrawable
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
@@ -134,7 +135,8 @@ open class LoadImageView @JvmOverloads constructor(
|
||||
clearRetry()
|
||||
task?.dispose()
|
||||
mRequest =
|
||||
ImageRequest.Builder(context).data(url).memoryCacheKey(key).diskCacheKey(key).target(
|
||||
ImageRequest.Builder(context).data(url).memoryCacheKey(key).diskCacheKey(key)
|
||||
.size(Size.ORIGINAL).target(
|
||||
{ onWait() },
|
||||
{ onFailure() },
|
||||
{ onGetValue(it, isLoaded) }
|
||||
|
||||
@@ -16,22 +16,10 @@
|
||||
|
||||
package com.hippo.yorozuya;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Utilities {
|
||||
|
||||
private static final String[] MIME_TYPE_ARRAY = {
|
||||
"image/jpg"
|
||||
};
|
||||
private static final String[] EXTENSION_ARRAY = {
|
||||
"jpg"
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the array contain the element
|
||||
*
|
||||
@@ -71,48 +59,4 @@ public final class Utilities {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getNameFromUrl(String url) {
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
int fragment = url.lastIndexOf('#');
|
||||
if (fragment > 0) {
|
||||
url = url.substring(0, fragment);
|
||||
}
|
||||
|
||||
int query = url.lastIndexOf('?');
|
||||
if (query > 0) {
|
||||
url = url.substring(0, query);
|
||||
}
|
||||
|
||||
int filenamePos = url.lastIndexOf('/');
|
||||
String filename =
|
||||
0 <= filenamePos ? url.substring(filenamePos + 1) : url;
|
||||
|
||||
// if the filename contains special characters, we don't
|
||||
// consider it valid for our matching purposes:
|
||||
if (filename.length() != 0 &&
|
||||
Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)%]+", filename)) {
|
||||
int dotPos = filename.lastIndexOf('.');
|
||||
if (0 < dotPos) {
|
||||
return filename.substring(0, dotPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getExtensionFromMimeType(String mimeType) {
|
||||
if (TextUtils.isEmpty(mimeType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0, n = MIME_TYPE_ARRAY.length; i < n; i++) {
|
||||
if (MIME_TYPE_ARRAY[i].equals(mimeType)) {
|
||||
return EXTENSION_ARRAY[i];
|
||||
}
|
||||
}
|
||||
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
}
|
||||
@@ -988,6 +988,10 @@ class ReaderActivity : EhActivity() {
|
||||
.onEach { setPageNumberVisibility(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.showReaderSeekbar().changes()
|
||||
.onEach { setReaderSeekbarVisibility(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
readerPreferences.trueColor().changes()
|
||||
.onEach { setTrueColor(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -1050,6 +1054,10 @@ class ReaderActivity : EhActivity() {
|
||||
binding.pageNumber.isVisible = visible
|
||||
}
|
||||
|
||||
fun setReaderSeekbarVisibility(visible: Boolean) {
|
||||
binding.readerSeekbar.isVisible = visible
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 32-bit color mode according to [enabled].
|
||||
*/
|
||||
|
||||
@@ -99,7 +99,7 @@ abstract class PageLoader {
|
||||
}
|
||||
|
||||
override fun sizeOf(key: Int, value: Image): Int {
|
||||
return value.height * value.width * 4
|
||||
return value.size
|
||||
}
|
||||
|
||||
override fun entryRemoved(evicted: Boolean, key: Int, oldValue: Image, newValue: Image?) {
|
||||
|
||||
@@ -35,6 +35,7 @@ class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: A
|
||||
private fun initGeneralPreferences() {
|
||||
binding.backgroundColor.bindToIntPreference(readerPreferences.readerTheme(), R.array.reader_themes_values)
|
||||
binding.showPageNumber.bindToPreference(readerPreferences.showPageNumber())
|
||||
binding.showReaderSeekbar.bindToPreference(readerPreferences.showReaderSeekbar())
|
||||
binding.fullscreen.bindToPreference(readerPreferences.fullscreen())
|
||||
readerPreferences.fullscreen()
|
||||
.asHotFlow {
|
||||
|
||||
@@ -14,7 +14,7 @@ class ReaderPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true)
|
||||
|
||||
fun showReadingMode() = preferenceStore.getBoolean("pref_show_reading_mode", true)
|
||||
fun showReaderSeekbar() = preferenceStore.getBoolean("pref_show_reader_seekbar", true)
|
||||
|
||||
fun trueColor() = preferenceStore.getBoolean("pref_true_color_key", false)
|
||||
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
app:defaultNavHost="true" />
|
||||
</com.hippo.ehviewer.widget.AboveSnackerLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
app:columnCount="3"
|
||||
app:itemMargin="8dp" />
|
||||
|
||||
<TextView
|
||||
<Button
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:id="@+id/preview_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
android:text="@string/pref_show_page_number"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/show_reader_seekbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/pref_show_reader_seekbar"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/fullscreen"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
<com.hippo.widget.FabLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="@dimen/corner_fab_margin"
|
||||
android:elevation="6dp"
|
||||
android:paddingEnd="@dimen/corner_fab_margin"
|
||||
android:paddingBottom="@dimen/corner_fab_margin"
|
||||
app:fitsSystemWindowsInsets="top|bottom">
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginEnd="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
@@ -34,7 +34,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginStart="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
@@ -45,17 +45,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginBottom="@dimen/advance_search_table_item_margin">
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/search_sr" />
|
||||
android:layout_marginBottom="@dimen/advance_search_table_item_margin"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
@@ -63,6 +54,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginEnd="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/search_sr">
|
||||
|
||||
@@ -70,6 +62,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
android:text="@string/unlimited"
|
||||
app:simpleItems="@array/search_min_rating" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -85,7 +78,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginEnd="@dimen/advance_search_table_item_margin"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/search_sp" />
|
||||
@@ -98,6 +91,7 @@
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLines="1" />
|
||||
|
||||
@@ -118,6 +112,7 @@
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:imeOptions="actionNone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLines="1" />
|
||||
|
||||
@@ -125,7 +120,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginStart="@dimen/advance_search_table_item_margin"
|
||||
android:text="@string/search_sp_suffix"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
</LinearLayout>
|
||||
@@ -153,7 +148,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginRight="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginEnd="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
@@ -174,7 +169,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginStart="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
<!-- German translations by UShimamura -->
|
||||
<resources>
|
||||
<string name="app_name">EhViewer</string>
|
||||
<string name="homepage">Startseite</string>
|
||||
<string name="subscription">Gefolgte Etiketten</string>
|
||||
<string name="whats_hot">Beliebt</string>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Category -->
|
||||
|
||||
<string name="homepage">Página de inicio</string>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
<!-- French translations by UShimamura -->
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Category -->
|
||||
|
||||
<string name="homepage">Page d\'acceuil</string>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
<!-- Japanese translations by UShimamura, trhjew, Rouryi and onlymash -->
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<string name="homepage">ホームページ</string>
|
||||
<string name="subscription">タグの購読</string>
|
||||
<string name="whats_hot">人気</string>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Theme -->
|
||||
|
||||
<string name="homepage">홈</string>
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="text_is_empty">Tekst tom</string>
|
||||
<string name="waring">Advarsel</string>
|
||||
<string name="clipboard_gallery_url_snack_action">Vis</string>
|
||||
<string name="error_bad_status_code">Feil statuskode: %d</string>
|
||||
<string name="error_timeout">Tidsavbrudd</string>
|
||||
<string name="error_unknown_host">Ukjent vert</string>
|
||||
<string name="error_redirection">For mange videresendinger</string>
|
||||
<string name="error_socket">Nettverksfeil</string>
|
||||
<string name="password">Passord</string>
|
||||
<string name="sign_in_via_webview">Logg inn via WebView</string>
|
||||
<string name="downloads">Nedlastninger</string>
|
||||
<string name="register">Registrering</string>
|
||||
<string name="error_invalid_number">Ugyldig nummer</string>
|
||||
<string name="similar_gallery">Lignende</string>
|
||||
<string name="homepage">Hjemmeside</string>
|
||||
<string name="history">Historikk</string>
|
||||
<string name="subscription">Abonnement</string>
|
||||
<string name="settings">Innstillinger</string>
|
||||
<string name="username">Brukernavn</string>
|
||||
<string name="sign_in_via_cookies">Logg inn via informasjonskapsler</string>
|
||||
<string name="sign_in">Logg inn</string>
|
||||
<string name="sign_in_first">Logg inn først</string>
|
||||
<string name="download">Last ned</string>
|
||||
<string name="quick_search">Hurtigsøk</string>
|
||||
<string name="star_2">2 stjerner</string>
|
||||
<string name="readme">LESMEG</string>
|
||||
<string name="star_4">4 stjerner</string>
|
||||
<string name="no_torrents">Ingen torrenter</string>
|
||||
<string name="search_sfl">Språk</string>
|
||||
<string name="search_osc">Kun søk i omslag</string>
|
||||
<string name="error_empty_html">Tom side</string>
|
||||
<string name="tourist_mode">Fortsett uten å logge inn</string>
|
||||
<string name="select_scene">Hvilken galleriside vil du besøke\?</string>
|
||||
<string name="favored_times">♥ %d</string>
|
||||
<string name="torrent_count">Torrent (%d)</string>
|
||||
<string name="share">Del</string>
|
||||
<string name="gallery_list_search_bar_open_gallery">Åpne galleriet</string>
|
||||
<string name="search_normal_search">Normalt søk</string>
|
||||
<string name="search_subscription_search">Abonnementssøk</string>
|
||||
<string name="search_sp_to">til</string>
|
||||
<string name="search_sfu">Opplaster</string>
|
||||
<string name="delete">Slett</string>
|
||||
<string name="search">Søk</string>
|
||||
<string name="image_search">Bildesøk</string>
|
||||
<string name="search_normal">Normalt søk</string>
|
||||
<string name="search_cover">Søk i omslag</string>
|
||||
<string name="search_image">Bildesøk</string>
|
||||
<string name="search_specify_uploader">Angi opplaster</string>
|
||||
<string name="search_specify_tag">Angi etikett</string>
|
||||
<string name="select_image">Velg bilde</string>
|
||||
<string name="star_3">3 stjerner</string>
|
||||
<string name="read">Les</string>
|
||||
<string name="no_comments">Ingen kommentarer</string>
|
||||
<string name="duplicate_name">Dette navnet er allerede i bruk.</string>
|
||||
<string name="star_5">5 stjerner</string>
|
||||
<string name="no_tags">Ingen etiketter</string>
|
||||
<string name="no_previews">Ingen forhåndsvisninger</string>
|
||||
<string name="no_more_previews">Ingen flere forhåndsvisninger</string>
|
||||
<string name="no_more_comments">Ingen flere kommentarer</string>
|
||||
<string name="open_in_other_app">Åpne med et annet program</string>
|
||||
<string name="torrents">Torrenter</string>
|
||||
<string name="more_previews">Flere forhåndsvisninger</string>
|
||||
<string name="refresh">Gjenoppfrisk</string>
|
||||
<string name="get_it">Skjønner</string>
|
||||
<string name="keyword_search">Nøkkelordssøk</string>
|
||||
<string name="search_sp">Sider:</string>
|
||||
<string name="search_sft">Etiketter</string>
|
||||
<string name="unlimited">Ubegrenset</string>
|
||||
<plurals name="page_count">
|
||||
<item quantity="one">%d side</item>
|
||||
<item quantity="other">%d sider</item>
|
||||
</plurals>
|
||||
<string name="more_comment">Flere kommentarer</string>
|
||||
<string name="tag_vote_up">Stem opp</string>
|
||||
<string name="pref_grayscale">Gråskala</string>
|
||||
<string name="action_share">Del</string>
|
||||
<string name="download_torrent_started">Nedlasting av torrent startet</string>
|
||||
<string name="filter_the_tag">Blokker etiketten «%s»\?</string>
|
||||
<string name="rating9">Underbar</string>
|
||||
<string name="error_invalid_url">Ugyldig nettadresse</string>
|
||||
<string name="error_cannot_parse_the_url">Kan ikke tolke nettadressen</string>
|
||||
<string name="rating4">Dårlig</string>
|
||||
<string name="error_cant_find_activity">Finner ikke programmet</string>
|
||||
<string name="error_509">509</string>
|
||||
<string name="rating7">Bra</string>
|
||||
<string name="rating5">Middelmådig</string>
|
||||
<string name="rating3">Elending</string>
|
||||
<string name="error_decoding_failed">Dekoding mislyktes</string>
|
||||
<string name="error_reading_failed">Lesing mislyktes</string>
|
||||
<string name="error_parse_error">Tolkningsfeil</string>
|
||||
<string name="filter_the_uploader">Blokker opplasteren «%s»\?</string>
|
||||
<string name="error_write_failed">Skriving mislyktes</string>
|
||||
<string name="rating10">Mesterverk</string>
|
||||
<string name="rating8">Storslått</string>
|
||||
<string name="rating6">OK</string>
|
||||
<string name="rating2">Pinlig</string>
|
||||
<string name="key_pages">Sider</string>
|
||||
<string name="invalid_ip">Ugyldig IP</string>
|
||||
<string name="share_image">Del bilde</string>
|
||||
<string name="settings_eh_sign_out">Logg ut</string>
|
||||
<string name="settings_eh_identity_cookies_tourist">Turistmodus</string>
|
||||
<string name="settings_eh_identity_cookies_copy">Kopier informasjonskapsler</string>
|
||||
<string name="settings_about_version">Byggversjon</string>
|
||||
<string name="settings_my_tags">Mine etiketter</string>
|
||||
<string name="settings_eh_tag_translations_source">Plassholder</string>
|
||||
<string name="settings_eh_show_gallery_pages">Vis gallerisider</string>
|
||||
<string name="settings_advanced">Avansert</string>
|
||||
<string name="settings_about">Om</string>
|
||||
<string name="proxy_host_or_ip">Vert eller IP</string>
|
||||
<string name="settings_advanced_export_data">Eksporter data</string>
|
||||
<string name="settings_advanced_export_data_to">Data eksportert til %s</string>
|
||||
<string name="settings_advanced_import_data">Importer data</string>
|
||||
<string name="proxy_system">System-mellomtjener</string>
|
||||
<string name="settings_about_source">Kildekode</string>
|
||||
<string name="proxy_direct">Direkte</string>
|
||||
<string name="proxy_port">Port</string>
|
||||
<string name="proxy_invalid_port">Ugyldig port</string>
|
||||
<string name="rating1">Utolererbar</string>
|
||||
<string name="rating0">Katastrofe</string>
|
||||
<string name="gallery_info">Galleri-info</string>
|
||||
<string name="header_key">Nøkkel</string>
|
||||
<string name="add_host">Legg til en vert</string>
|
||||
<string name="settings_eh_list_mode">Listemodus</string>
|
||||
<string name="copied_to_clipboard">Kopiert til utklippstavlen</string>
|
||||
<string name="header_value">Verdi</string>
|
||||
<string name="key_title">Navn</string>
|
||||
<string name="stat_download_action_stop_all">Stopp alle</string>
|
||||
<string name="default_download_label_name">Forvalg</string>
|
||||
<string name="settings_u_config">EHentai-innstillinger</string>
|
||||
<string name="key_token">Symbol</string>
|
||||
<string name="key_language">Språk</string>
|
||||
<string name="go_to">Gå til</string>
|
||||
<string name="key_url">Nettadresse</string>
|
||||
<string name="cant_read_the_file">Kan ikke lese filen</string>
|
||||
<string name="key_category">Kategori</string>
|
||||
<string name="key_uploader">Opplaster</string>
|
||||
<string name="key_visible">Synlig</string>
|
||||
<string name="key_size">Størrelse</string>
|
||||
<string name="download_move_dialog_title">Flytt</string>
|
||||
<string name="key_torrents">Torrenter</string>
|
||||
<string name="no_one_comments_gallery">Ingen kommentar.</string>
|
||||
<string name="gallery_comments">Gallerikommentarer</string>
|
||||
<string name="download_state_wait">Venter</string>
|
||||
<string name="download_state_failed">Mislykket</string>
|
||||
<string name="download_state_failed_2">%d ufullstendig</string>
|
||||
<string name="download_labels">Last ned etiketter</string>
|
||||
<string name="download_start_all">Start alle</string>
|
||||
<string name="edit_comment">Rediger kommentar</string>
|
||||
<string name="stat_509_alert_title">509-advarsel</string>
|
||||
<string name="stat_download_done_title">Nedlasting fullført</string>
|
||||
<string name="gallery_previews">Galleriforhåndsvisninger</string>
|
||||
<string name="download_state_none">Pauset</string>
|
||||
<string name="download_state_downloading">Laster ned</string>
|
||||
<string name="download_state_downloaded">Nedlastet</string>
|
||||
<string name="download_state_finish">Ferdig</string>
|
||||
<string name="download_remove_dialog_check_text">Slett bildefiler</string>
|
||||
<string name="add_host_add">Legg til</string>
|
||||
<string name="settings_eh_gallery_site">Galleriside</string>
|
||||
<string name="download_stop_all">Stopp alle</string>
|
||||
<string name="invalid_host">Ugyldig vert</string>
|
||||
<string name="image_saved">Bilde lagret i %s</string>
|
||||
<string name="settings_download_download_origin_image">Last ned opprinnelig bilde</string>
|
||||
<string name="edit_host">Rediger verten</string>
|
||||
<string name="edit_host_confirm">Bekreft</string>
|
||||
<string name="edit_host_delete">Slett</string>
|
||||
<string name="settings_privacy_secure">Forhindre skjermavbildninger</string>
|
||||
<string name="download_service">Nedlastingstjeneste</string>
|
||||
<string name="proxy_type_name">Mellomtjenertype</string>
|
||||
<string name="action_copy">Kopier</string>
|
||||
<string name="toplist">Toppliste</string>
|
||||
<string name="toplist_pastyear">Forrige år</string>
|
||||
<string name="toplist_yesterday">I går</string>
|
||||
<string name="action_add_tag">Legg til etikett</string>
|
||||
<string name="reset_download_location">Tilbakestill til forvalg</string>
|
||||
<string name="dont_show_again">Ikke vis igjen</string>
|
||||
<string name="comment_user_uploader">%s (opplaster)</string>
|
||||
<string name="copy_trans">Kopier oversettelse</string>
|
||||
<string name="pref_rotation_type">Forvalgt rotasjonstype</string>
|
||||
<string name="viewer">Lesemodus</string>
|
||||
<string name="nav_zone_prev">Forrige</string>
|
||||
<string name="open_settings">Åpne innstillingene</string>
|
||||
<string name="open_by_default">Åpne som forvalg</string>
|
||||
<string name="settings_privacy_require_unlock">Krev opplåsing</string>
|
||||
<string name="archive_passwd">passord</string>
|
||||
<string name="account_name">Konto</string>
|
||||
<string name="action_settings">Innstillinger</string>
|
||||
<string name="nav_zone_left">Venstre</string>
|
||||
<string name="pref_fullscreen">Fullskjermsvisning</string>
|
||||
<string name="pref_inverted_colors">Invertert</string>
|
||||
<string name="action_menu">Meny</string>
|
||||
<string name="nav_zone_next">Neste</string>
|
||||
<string name="action_retry">Prøv igjen</string>
|
||||
<string name="label_default">Forvalg</string>
|
||||
<string name="tapping_inverted_horizontal">Vannrett</string>
|
||||
<string name="pref_category_reading_mode">Lesemodus</string>
|
||||
<string name="automatic_background">Automatisk</string>
|
||||
<string name="nav_zone_right">Høyre</string>
|
||||
<string name="pref_read_with_long_tap">Vis ved lang-trykk</string>
|
||||
<string name="pref_custom_color_filter">Egendefinert fargefilter</string>
|
||||
<string name="filter_mode_screen">Skjerm</string>
|
||||
<string name="pref_keep_screen_on">Behold skjerm påslått</string>
|
||||
<string name="tapping_inverted_vertical">Loddrett</string>
|
||||
<string name="tapping_inverted_both">Begge</string>
|
||||
<string name="white_background">Hvit</string>
|
||||
<string name="pref_reader_theme">Bakgrunnsfarge</string>
|
||||
<string name="black_background">Svart</string>
|
||||
<string name="gray_background">Grå</string>
|
||||
<string name="edge_nav">Kant</string>
|
||||
<string name="scale_type_stretch">Strekk</string>
|
||||
<string name="right_and_left_nav">Høyre og venstre</string>
|
||||
<string name="settings_eh_thumb_resolution_auto">Automatisk</string>
|
||||
<string name="db_version_too_high">Databaseversjonen er for høy</string>
|
||||
<string name="app_language_system">Systemspråk (forvalg)</string>
|
||||
<string name="error_something_wrong_happened">Noe gikk galt</string>
|
||||
<string name="need_sign_in">Innlogging kreves</string>
|
||||
<string name="just_now">Akkurat nå</string>
|
||||
<string name="move_favorites_dialog_title">Flytt favoritter</string>
|
||||
<string name="let_me_select">La meg velge</string>
|
||||
<string name="toplist_pastmonth">Forrige måned</string>
|
||||
<string name="from_clipboard">Fra utklippstavlen</string>
|
||||
<string name="delete_search_history">Slett «%s» fra søkehistorikk\?</string>
|
||||
<string name="collections">Samlinger</string>
|
||||
<string name="tag_vote_down">Stem ned</string>
|
||||
<plurals name="some_minutes_ago">
|
||||
<item quantity="one">Et minutt siden</item>
|
||||
<item quantity="other">%d minutter siden</item>
|
||||
</plurals>
|
||||
<plurals name="minute">
|
||||
<item quantity="one">min</item>
|
||||
<item quantity="other">min</item>
|
||||
</plurals>
|
||||
<plurals name="some_hours_ago">
|
||||
<item quantity="one">Én time siden</item>
|
||||
<item quantity="other">%d timer siden</item>
|
||||
</plurals>
|
||||
<string name="yesterday">I går</string>
|
||||
<string name="some_days_ago">%d dager siden</string>
|
||||
<plurals name="hour">
|
||||
<item quantity="one">time</item>
|
||||
<item quantity="other">timer</item>
|
||||
</plurals>
|
||||
<string name="download_all">Alle</string>
|
||||
<string name="action_save">Lagre</string>
|
||||
<plurals name="second">
|
||||
<item quantity="one">sek</item>
|
||||
<item quantity="other">sek</item>
|
||||
</plurals>
|
||||
<string name="settings_privacy">Personvern</string>
|
||||
<plurals name="day">
|
||||
<item quantity="one">dag</item>
|
||||
<item quantity="other">dager</item>
|
||||
</plurals>
|
||||
<plurals name="year">
|
||||
<item quantity="one">år</item>
|
||||
<item quantity="other">år</item>
|
||||
</plurals>
|
||||
<string name="no_archives">Ingen arkiver</string>
|
||||
<string name="action_save_to">Lagre i …</string>
|
||||
</resources>
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Theme -->
|
||||
|
||||
<string name="homepage">หน้าหลัก</string>
|
||||
|
||||
Executable → Regular
+17
-77
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Hippo Sevenerror_please_login_first
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,14 +13,8 @@
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
--><resources>
|
||||
<!-- Theme -->
|
||||
|
||||
<string name="homepage">主页</string>
|
||||
<string name="subscription">订阅</string>
|
||||
<string name="whats_hot">热门</string>
|
||||
@@ -27,26 +22,20 @@
|
||||
<string name="history">历史</string>
|
||||
<string name="downloads">下载</string>
|
||||
<string name="settings">设置</string>
|
||||
|
||||
<string name="username">用户名</string>
|
||||
<string name="password">密码</string>
|
||||
|
||||
<string name="sign_in">登录</string>
|
||||
<string name="register">注册</string>
|
||||
<string name="sign_in_via_webview">通过网页登录</string>
|
||||
<string name="sign_in_via_cookies">通过 Cookie 登录</string>
|
||||
<string name="sign_in_first">请先登录</string>
|
||||
|
||||
<string name="cookie_explain">Cookie 是存储在浏览器里的一小块数据。如果您不清楚 Cookie 什么或者怎么获取 Cookie,最好搜索一下。\n\n请输入特定的 Cookie 来登录。</string>
|
||||
<string name="text_is_empty">文本为空</string>
|
||||
<string name="waring">警告</string>
|
||||
<string name="wrong_cookie_warning">看起来您输入的 Cookie 是错误的,最好检查一下。</string>
|
||||
|
||||
<string name="invalid_download_location">似乎下载路径不可用。请到重新设置下载路径。</string>
|
||||
|
||||
<string name="clipboard_gallery_url_snack_message">剪切板里有画廊链接</string>
|
||||
<string name="clipboard_gallery_url_snack_action">查看</string>
|
||||
|
||||
<string name="error_bad_status_code">错误状态码:%d</string>
|
||||
<string name="error_timeout">超时</string>
|
||||
<string name="error_unknown_host">未知主机</string>
|
||||
@@ -68,9 +57,7 @@
|
||||
<string name="error_invalid_number">非法数字</string>
|
||||
<string name="error_please_login_first">请先登陆</string>
|
||||
<string name="error_empty_html">页面内容为空</string>
|
||||
|
||||
<string name="kokomade_tip">设置->Eh->画廊站点->e-hentai</string>
|
||||
|
||||
<string name="kokomade_tip">设置->Eh->画廊站点->e-hentai</string>
|
||||
<string name="app_waring">本应用中内容来自互联网,部分内容可能对您的生理及心理造成难以恢复的伤害。本应用作者不会对由本应用造成的任何后果负责。\n\n未成年人应在监护人指导下使用本应用。\n\n继续使用即代表您同意上述条款。</string>
|
||||
<string name="error_username_cannot_empty">用户名不可为空</string>
|
||||
<string name="error_password_cannot_empty">密码不可为空</string>
|
||||
@@ -78,25 +65,19 @@
|
||||
<string name="sign_in_failed">登录失败</string>
|
||||
<string name="sign_in_failed_tip">若持续出错,请尝试“通过网页登录”或“通过 Cookie 登录”。</string>
|
||||
<string name="get_it">知道了</string>
|
||||
|
||||
<!-- Select scene -->
|
||||
<string name="select_scene">你想选择哪个画廊站点?</string>
|
||||
<string name="select_scene_explain">e-hentai:对任何人开放\nexhentai:仅对登录用户开放</string>
|
||||
|
||||
<!-- Update -->
|
||||
|
||||
<!-- Gallery List -->
|
||||
<string name="gallery_list_search_bar_hint_exhentai">搜索 ExHentai</string>
|
||||
<string name="gallery_list_search_bar_hint_e_hentai">搜索 E-Hentai</string>
|
||||
<string name="gallery_list_search_bar_open_gallery">打开画廊</string>
|
||||
<string name="gallery_list_empty_hit">什么都没有找到</string>
|
||||
<string name="gallery_list_empty_hit_subscription">请至 设置->EH->我的标签 订阅标签</string>
|
||||
|
||||
<string name="gallery_list_empty_hit_subscription">请至 设置->EH->我的标签 订阅标签</string>
|
||||
<string name="search">搜索</string>
|
||||
|
||||
<string name="keyword_search">关键字搜索</string>
|
||||
<string name="image_search">图片搜索</string>
|
||||
|
||||
<string name="search_normal">普通搜索</string>
|
||||
<string name="search_advance">高级选项</string>
|
||||
<string name="search_image">图片搜索</string>
|
||||
@@ -105,9 +86,7 @@
|
||||
<string name="search_specify_uploader">指定上传者</string>
|
||||
<string name="search_specify_tag">指定标签</string>
|
||||
<string name="search_enable_advance">启用高级选项</string>
|
||||
|
||||
<string name="search_tip">一般搜索:就是搜索。\n\n订阅搜素:在订阅中搜索。\n\n指定上传者:列出该上传者上传的画廊。其他选项会被忽略。\n\n指定标签:列出含有该标签的画廊。其他选项会被忽略。</string>
|
||||
|
||||
<string name="search_sh">仅显示被删除的画廊</string>
|
||||
<string name="search_sto">仅显示有种子的画廊</string>
|
||||
<string name="search_sr">最低评分:</string>
|
||||
@@ -119,17 +98,13 @@
|
||||
<string name="search_sfl">语言</string>
|
||||
<string name="search_sfu">上传者</string>
|
||||
<string name="search_sft">标签</string>
|
||||
|
||||
<string name="select_image">选择图片</string>
|
||||
<string name="search_uss">使用相似搜索</string>
|
||||
<string name="search_osc">只搜索封面</string>
|
||||
<string name="select_image_first">请先选择图片</string>
|
||||
|
||||
<string name="add_to_favourites">收藏</string>
|
||||
<string name="remove_from_favourites">移除收藏</string>
|
||||
|
||||
<string name="delete_downloads">删除下载</string>
|
||||
|
||||
<string name="quick_search">快速搜索</string>
|
||||
<string name="quick_search_tip">点击“+”来添加快速搜索</string>
|
||||
<string name="add_quick_search_dialog_title">添加快速搜索</string>
|
||||
@@ -142,15 +117,13 @@
|
||||
<string name="duplicate_name">已存在相同名称</string>
|
||||
<string name="save_progress">保存进度</string>
|
||||
<string name="delete_quick_search">删除快速搜索“%s”?</string>
|
||||
|
||||
<string name="go_to_hint">第 %1$d 页,共 %2$d 页</string>
|
||||
|
||||
<!-- search_min_rating -->
|
||||
<string name="unlimited">不限</string>
|
||||
<string name="star_2">2 星</string>
|
||||
<string name="star_3">3 星</string>
|
||||
<string name="star_4">4 星</string>
|
||||
<string name="star_5">5 星</string>
|
||||
|
||||
<!-- Gallery detail -->
|
||||
<string name="error_cannot_find_gallery">找不到画廊</string>
|
||||
<string name="download">下载</string>
|
||||
@@ -193,7 +166,6 @@
|
||||
<string name="filter_added">已添加屏蔽项</string>
|
||||
<string name="newer_version_avaliable">此画廊有新版本可用。</string>
|
||||
<string name="newer_version_title">%1$s, 添加于 %2$s</string>
|
||||
|
||||
<!-- Rating -->
|
||||
<string name="rating10">根本把持不住</string>
|
||||
<string name="rating9">好极了</string>
|
||||
@@ -206,13 +178,12 @@
|
||||
<string name="rating2">瞎眼</string>
|
||||
<string name="rating1">快要窒息了</string>
|
||||
<string name="rating0">…</string>
|
||||
|
||||
<!-- Gallery Info -->
|
||||
<string name="gallery_info">画廊信息</string>
|
||||
<string name="copied_to_clipboard">已复制到剪切板</string>
|
||||
<string name="header_key">键</string>
|
||||
<string name="header_value">值</string>
|
||||
<string name="key_gid">Gid</string>
|
||||
<string name="key_gid">GID</string>
|
||||
<string name="key_token">Token</string>
|
||||
<string name="key_url">链接</string>
|
||||
<string name="key_title">标题</string>
|
||||
@@ -231,8 +202,7 @@
|
||||
<string name="key_rating_count">评价次数</string>
|
||||
<string name="key_rating">评分</string>
|
||||
<string name="key_torrents">种子个数</string>
|
||||
<string name="key_torrent_url">种子</string>
|
||||
|
||||
<string name="key_torrent_url">种子链接</string>
|
||||
<!-- Gallery Comments -->
|
||||
<string name="gallery_comments">画廊评论</string>
|
||||
<string name="no_one_comments_gallery">暂时没有评论</string>
|
||||
@@ -256,11 +226,9 @@
|
||||
<string name="check_vote_status">查看投票情况</string>
|
||||
<string name="click_more_comments">点击加载更多评论</string>
|
||||
<string name="last_edited">上次修改时间:%s</string>
|
||||
|
||||
<!-- Gallery Previews -->
|
||||
<string name="gallery_previews">画廊预览</string>
|
||||
<string name="go_to">跳页</string>
|
||||
|
||||
<!-- Download -->
|
||||
<string name="scene_download_title">下载 - %s</string>
|
||||
<string name="no_download_info">这里是下载项目</string>
|
||||
@@ -297,7 +265,6 @@
|
||||
<string name="remember_download_label">记住下载标签</string>
|
||||
<string name="default_download_label">默认下载标签</string>
|
||||
<string name="added_to_download_list">已添加至下载列表</string>
|
||||
|
||||
<!-- Download label -->
|
||||
<string name="add">添加</string>
|
||||
<string name="new_label_title">新标签</string>
|
||||
@@ -306,12 +273,10 @@
|
||||
<string name="label_text_exist">标签已存在</string>
|
||||
<string name="rename_label_title">重命名标签</string>
|
||||
<string name="delete_label">删除标签“%s”?</string>
|
||||
|
||||
<!-- History -->
|
||||
<string name="no_history">这里是阅读历史</string>
|
||||
<string name="clear_all">全部清除</string>
|
||||
<string name="clear_all_history">清除所有阅读历史?</string>
|
||||
|
||||
<!-- Filter -->
|
||||
<string name="filter">屏蔽列表</string>
|
||||
<string name="filter_title">标题</string>
|
||||
@@ -326,17 +291,13 @@
|
||||
<string name="show_definition">查看定义</string>
|
||||
<string name="filter_text">屏蔽项文本</string>
|
||||
<string name="filter_tip">该屏蔽系统会在 EHentai 网站屏蔽系统的基础上继续屏蔽画廊。\n\n标题屏蔽项:排除标题含有该关键字的画廊。\n\n上传者屏蔽项:排除该上传者上传的画廊。\n\n标签屏蔽项:排除包含该标签的画廊,这会使获取画廊列表花费更多时间。\n\n标签组屏蔽项:排除包含该标签组的画廊,这会使获取画廊列表花费更多时间。\n\n评论者屏蔽项:排除该评论者发布的评论。\n\n评论屏蔽项:排除匹配该正则表达式的评论。</string>
|
||||
|
||||
<!-- Set Security -->
|
||||
|
||||
<!-- UConfig Activity -->
|
||||
<string name="u_config">EHentai 设置</string>
|
||||
<string name="apply">应用</string>
|
||||
<string name="apply_tip">点击右上角的对勾来保存设置</string>
|
||||
|
||||
<!-- My Tags Activity -->
|
||||
<string name="my_tags">我的标签</string>
|
||||
|
||||
<!-- Hosts Activity -->
|
||||
<string name="hosts">自定义 hosts.txt</string>
|
||||
<string name="hosts_host">Host</string>
|
||||
@@ -348,11 +309,9 @@
|
||||
<string name="edit_host_delete">删除</string>
|
||||
<string name="invalid_host">无效的 host</string>
|
||||
<string name="invalid_ip">无效的 IP</string>
|
||||
|
||||
<!-- Gallery Activity -->
|
||||
<string name="share_image">分享图片</string>
|
||||
<string name="image_saved">图片已保存至 %s</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings_eh">EH</string>
|
||||
<string name="settings_eh_sign_out">退出登录</string>
|
||||
@@ -386,14 +345,10 @@
|
||||
<string name="settings_eh_show_tag_translations_summary">显示翻译后的标签而非原始文字(需花费时间来下载数据文件)</string>
|
||||
<string name="settings_eh_tag_translations_source">补充翻译(由 EhTagTranslation 提供)</string>
|
||||
<string name="settings_eh_tag_translations_source_url">https://github.com/EhTagTranslation/Editor/wiki</string>
|
||||
<string name="settings_eh_preview_num">画廊详情页预览图最大数量</string>
|
||||
<string name="settings_eh_preview_num_summary">由于历史因素,此值过大会导致卡顿</string>
|
||||
<string name="settings_eh_filter">屏蔽列表</string>
|
||||
<string name="settings_eh_filter_summary">根据标题、上传者、标签、评论者屏蔽画廊或评论</string>
|
||||
|
||||
<string name="settings_read_volume_page">使用音量键翻页</string>
|
||||
<string name="settings_read_reverse_volume">翻转音量键</string>
|
||||
|
||||
<string name="settings_download">下载</string>
|
||||
<string name="settings_download_download_location">下载路径</string>
|
||||
<string name="settings_download_invalid_download_location">无效的下载路径</string>
|
||||
@@ -419,7 +374,6 @@
|
||||
<string name="settings_download_clean_redundancy_summary">清理下载目录中不在下载列表里的图片文件</string>
|
||||
<string name="settings_download_clean_redundancy_no_redundancy">未发现冗余</string>
|
||||
<string name="settings_download_clean_redundancy_done">完成冗余清理,共清理 %d 项</string>
|
||||
|
||||
<string name="settings_advanced">高级</string>
|
||||
<string name="settings_advanced_save_parse_error_body">解析失败时保存页面内容</string>
|
||||
<string name="settings_advanced_save_parse_error_body_summary">页面内容可能含有隐私敏感信息</string>
|
||||
@@ -438,6 +392,10 @@
|
||||
<string name="settings_advanced_built_in_hosts_summary">应用提供的主机到IP的映射\n可被自定义 hosts.txt 覆盖</string>
|
||||
<string name="settings_advanced_custom_hosts_title">自定义 hosts.txt</string>
|
||||
<string name="settings_advanced_custom_hosts_summary">将主机名称映射到相应的IP地址\n可覆盖内置 hosts.txt</string>
|
||||
<string name="settings_advanced_domain_fronting_title">域名前置(重启应用后生效)</string>
|
||||
<string name="settings_advanced_domain_fronting_summary">绕过 SNI 封锁\n启用时会禁用代理设置</string>
|
||||
<string name="settings_advanced_bypass_vpn_title">绕过VPN</string>
|
||||
<string name="settings_advanced_bypass_vpn_summary">启用域名前置时尝试绕过VPN</string>
|
||||
<string name="settings_advanced_export_data">导出数据</string>
|
||||
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
|
||||
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
|
||||
@@ -451,7 +409,6 @@
|
||||
<string name="settings_advanced_backup_favorite_nothing">没有可以备份的收藏列表</string>
|
||||
<string name="settings_advanced_backup_favorite_success">备份收藏列表成功</string>
|
||||
<string name="settings_advanced_backup_favorite_failed">备份收藏列表失败</string>
|
||||
|
||||
<string name="settings_about">关于</string>
|
||||
<string name="settings_about_declaration_summary">EhViewer 与 E-Hentai.org 无任何联系</string>
|
||||
<string name="settings_about_author">作者</string>
|
||||
@@ -459,26 +416,18 @@
|
||||
<string name="settings_about_source">源码</string>
|
||||
<string name="settings_about_version">版本号</string>
|
||||
<string name="settings_about_check_for_updates">检查更新</string>
|
||||
|
||||
<!-- ExcludedLanguagesScene -->
|
||||
|
||||
<string name="please_wait">请稍候</string>
|
||||
|
||||
<string name="proxy_direct">直接连接</string>
|
||||
<string name="proxy_system">系统代理</string>
|
||||
<string name="proxy_host_or_ip">主机或 IP</string>
|
||||
<string name="proxy_port">端口</string>
|
||||
<string name="proxy_invalid_port">无效的端口</string>
|
||||
|
||||
<string name="cant_read_the_file">无法读取文件</string>
|
||||
<string name="db_version_too_high">数据库版本过高</string>
|
||||
|
||||
<string name="app_language_system">系统语言(默认)</string>
|
||||
|
||||
<!-- License -->
|
||||
|
||||
<!-- Dir Picker-->
|
||||
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">云端收藏</string>
|
||||
<string name="local_favorites">本地收藏</string>
|
||||
@@ -492,12 +441,9 @@
|
||||
<string name="let_me_select">让我选择</string>
|
||||
<string name="collections">收藏夹</string>
|
||||
<string name="need_sign_in">需要登录</string>
|
||||
|
||||
<!-- Progress Scene -->
|
||||
<string name="error_something_wrong_happened">被玩坏了</string>
|
||||
|
||||
<!-- Guide -->
|
||||
|
||||
<!-- Readable Time -->
|
||||
<string name="from_the_future">来自未来</string>
|
||||
<string name="just_now">刚刚</string>
|
||||
@@ -509,7 +455,6 @@
|
||||
</plurals>
|
||||
<string name="yesterday">昨天</string>
|
||||
<string name="some_days_ago">%d 天前</string>
|
||||
|
||||
<plurals name="second">
|
||||
<item quantity="other">秒</item>
|
||||
</plurals>
|
||||
@@ -533,20 +478,19 @@
|
||||
<string name="archive_original">原始档案</string>
|
||||
<string name="archive_resample">重采样档案</string>
|
||||
<string name="download_archive_failure_no_hath">下载压缩包需要 H@H 客户端</string>
|
||||
<string name="current_funds">资金:%s GP,%d Credits</string>
|
||||
<string name="insufficient_funds">余额不足</string>
|
||||
<string name="settings_privacy">隐私</string>
|
||||
<string name="settings_privacy_secure">不允许屏幕抓取</string>
|
||||
<string name="settings_privacy_secure_summary">启用后,将不能截取该应用的屏幕截图,同时,将不会在系统任务切换器中显示该应用的内容预览,重新加载应用以生效此更改</string>
|
||||
|
||||
<string name="download_service">下载服务</string>
|
||||
<string name="favorite_name">收藏画廊</string>
|
||||
|
||||
<string name="black_dark_theme">纯黑深色主题</string>
|
||||
<string name="download_filter">过滤</string>
|
||||
<string name="download_all">全部</string>
|
||||
<string name="download_start_all_reversed">全部开始(倒序)</string>
|
||||
<string name="settings_download_download_delay">下载延时</string>
|
||||
<string name="settings_download_download_delay_summary">每次下载延时 %s 毫秒</string>
|
||||
|
||||
<string name="no_browser_installed">请安装一个浏览器。</string>
|
||||
<string name="toplist_alltime">从始至终</string>
|
||||
<string name="toplist_pastyear">过去一年</string>
|
||||
@@ -559,8 +503,6 @@
|
||||
<string name="from_clipboard">从剪贴板导入</string>
|
||||
<string name="from_clipboard_error">未在剪贴板中发现 Cookies。</string>
|
||||
<string name="delete_search_history">从搜索记录中删除“%s”?</string>
|
||||
<string name="settings_advanced_domain_fronting_title">域名前置(重启应用后生效)</string>
|
||||
<string name="settings_advanced_domain_fronting_summary">绕过 SNI 封锁\n启用时会禁用代理</string>
|
||||
<string name="action_add_tag">添加标签</string>
|
||||
<string name="action_add_tag_tip">添加新标签,用逗号「,」分割</string>
|
||||
<string name="comment_user_uploader">%s (上传者)</string>
|
||||
@@ -577,32 +519,30 @@
|
||||
<string name="app_link_not_verified_message">对于 Android 12 及更新的版本,您需要手动添加链接到已验证链接才能在 EhViewer 中打开 E-Hentai 链接。</string>
|
||||
<string name="app_link_not_verified_title">应用链接未验证</string>
|
||||
<string name="open_by_default">默认打开</string>
|
||||
|
||||
<string name="settings_privacy_require_unlock">需要解锁</string>
|
||||
<string name="settings_privacy_require_unlock_delay">锁定延迟</string>
|
||||
<string name="settings_privacy_require_unlock_delay_summary">离开应用并在 %s 分钟内返回时不需要解锁</string>
|
||||
<string name="settings_privacy_require_unlock_delay_summary_immediately">无论何时回到应用均要求解锁</string>
|
||||
<string name="proxy_type_name">代理类型</string>
|
||||
<string name="filter_label">屏蔽项类型</string>
|
||||
|
||||
<string name="archive_passwd">归档文件密码</string>
|
||||
<string name="archive_need_passwd">归档文件需要密码</string>
|
||||
<string name="passwd_wrong">密码错误</string>
|
||||
<string name="passwd_cannot_be_empty">密码不能为空</string>
|
||||
<string name="list_tile_thumb_size">详情模式下缩略图大小</string>
|
||||
<string name="account_name">账户</string>
|
||||
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="action_settings">设置</string>
|
||||
<string name="action_retry">重试</string>
|
||||
<string name="action_share">分享</string>
|
||||
<string name="action_copy">复制</string>
|
||||
<string name="action_save">保存</string>
|
||||
<string name="action_save_to">保存到…</string>
|
||||
<string name="action_save_to">保存到…</string>
|
||||
<string name="pref_category_general">通用</string>
|
||||
<string name="pref_fullscreen">全屏</string>
|
||||
<string name="pref_page_transitions">页面过渡动画</string>
|
||||
<string name="pref_show_page_number">显示页码</string>
|
||||
<string name="pref_show_reader_seekbar">显示跳页拖动条</string>
|
||||
<string name="pref_crop_borders">裁剪边缘</string>
|
||||
<string name="pref_custom_brightness">自定义亮度</string>
|
||||
<string name="pref_custom_color_filter">自定义滤镜</string>
|
||||
@@ -681,4 +621,4 @@
|
||||
<string name="disabled_nav">关闭</string>
|
||||
<string name="wide_color_gamut">使用Display P3色彩空间</string>
|
||||
<!-- Tachiyomi Reader End-->
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Theme -->
|
||||
|
||||
<string name="homepage">主頁</string>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
<!-- Theme -->
|
||||
|
||||
<string name="homepage">首頁</string>
|
||||
@@ -113,7 +111,9 @@
|
||||
<string name="search_sr">最低評分:</string>
|
||||
<string name="search_sp">頁數:</string>
|
||||
<string name="search_sp_to">到</string>
|
||||
<string name="search_sf">禁用排除項:</string>
|
||||
<string name="search_sp_err1">頁數最大值至少為 10</string>
|
||||
<string name="search_sp_err2">頁數範圍差至少為 20</string>
|
||||
<string name="search_sf">停用排除項:</string>
|
||||
<string name="search_sfl">語言</string>
|
||||
<string name="search_sfu">上傳者</string>
|
||||
<string name="search_sft">標籤</string>
|
||||
@@ -138,6 +138,8 @@
|
||||
<string name="image_search_not_quick_search">無法將圖片搜尋加入快速搜尋</string>
|
||||
<string name="duplicate_quick_search">已存在相同的快速搜尋,名稱為“%s”。</string>
|
||||
<string name="duplicate_name">已經由重複的名稱了</string>
|
||||
<string name="save_progress">儲存進度</string>
|
||||
<string name="delete_quick_search">刪除快速搜尋“%s”?</string>
|
||||
|
||||
<string name="go_to_hint">第 %1$d 頁,共 %2$d 頁</string>
|
||||
|
||||
@@ -173,6 +175,8 @@
|
||||
<string name="rate_successfully">評分成功</string>
|
||||
<string name="rate_failed">評分失敗</string>
|
||||
<string name="no_torrents">沒有種子</string>
|
||||
<string name="download_torrent_started">開始下載種子</string>
|
||||
<string name="download_torrent_failure">無法下載種子</string>
|
||||
<string name="torrents">種子</string>
|
||||
<string name="favorited">已收藏</string>
|
||||
<string name="not_favorited">未收藏</string>
|
||||
@@ -233,6 +237,8 @@
|
||||
<string name="comment_successfully">留言成功</string>
|
||||
<string name="comment_failed">留言失敗</string>
|
||||
<string name="copy_comment_text">複製留言文字</string>
|
||||
<string name="block_commenter">隱藏評論者</string>
|
||||
<string name="filter_the_commenter">隱藏評論者“%s”?</string>
|
||||
<string name="edit_comment">修改留言</string>
|
||||
<string name="edit_comment_successfully">留言已被修改</string>
|
||||
<string name="edit_comment_failed">修改留言失敗</string>
|
||||
@@ -297,6 +303,7 @@
|
||||
<string name="label_text_is_invalid">“預設”是無效的標籤</string>
|
||||
<string name="label_text_exist">標籤已存在</string>
|
||||
<string name="rename_label_title">重新命名標籤</string>
|
||||
<string name="delete_label">刪除標籤“%s”?</string>
|
||||
|
||||
<!-- History -->
|
||||
<string name="no_history">歷史紀錄將會顯示在這裡</string>
|
||||
@@ -304,17 +311,19 @@
|
||||
<string name="clear_all_history">清除所有歷史紀錄?</string>
|
||||
|
||||
<!-- Filter -->
|
||||
<string name="filter">遮蔽圖庫</string>
|
||||
<string name="filter">隱藏圖庫</string>
|
||||
<string name="filter_title">標題</string>
|
||||
<string name="filter_uploader">上傳者</string>
|
||||
<string name="filter_tag">標籤</string>
|
||||
<string name="filter_tag_namespace">標籤組</string>
|
||||
<string name="delete_filter">刪除遮蔽項“%s”?</string>
|
||||
<string name="filter_commenter">評論者</string>
|
||||
<string name="filter_comment">評論</string>
|
||||
<string name="delete_filter">刪除隱藏項“%s”?</string>
|
||||
<string name="tip">提示</string>
|
||||
<string name="add_filter">新增遮蔽項</string>
|
||||
<string name="add_filter">新增隱藏項</string>
|
||||
<string name="show_definition">檢視定義</string>
|
||||
<string name="filter_text">遮蔽項文字</string>
|
||||
<string name="filter_tip">該遮蔽系統會在 EHentai 網站遮蔽系統的基礎上繼續遮蔽圖庫。\n\n標題遮蔽項:排除標題含有該關鍵字的圖庫。\n\n上傳者遮蔽項:排除該上傳者上傳的圖庫。\n\n標籤遮蔽項:排除包含該標籤的圖庫,這會使獲取圖庫列表花費更多時間。\n\n標籤組遮蔽項:排除包含該標籤組的圖庫,這會使獲取圖庫列表花費更多時間。</string>
|
||||
<string name="filter_text">隱藏項文字</string>
|
||||
<string name="filter_tip">該隱藏系統會在 EHentai 網站隱藏系統的基礎上繼續隱藏圖庫。\n\n標題隱藏項:排除標題含有該關鍵字的圖庫。\n\n上傳者隱藏項:排除該上傳者上傳的圖庫。\n\n標籤隱藏項:排除包含該標籤的圖庫,這會使獲取圖庫列表花費更多時間。\n\n標籤組隱藏項:排除包含該標籤組的圖庫,這會使獲取圖庫列表花費更多時間。</string>
|
||||
|
||||
<!-- Set Security -->
|
||||
|
||||
@@ -369,12 +378,14 @@
|
||||
<string name="settings_eh_show_jpn_title_summary">需同時在 EHentai 網站設定中啟用 Japanese Title</string>
|
||||
<string name="settings_eh_show_gallery_pages">顯示圖庫頁數</string>
|
||||
<string name="settings_eh_show_gallery_pages_summary">在圖庫列表中顯示頁數</string>
|
||||
<string name="settings_eh_show_gallery_comments">顯示畫廊評論</string>
|
||||
<string name="settings_eh_show_gallery_comments_summary">在畫廊詳情頁中顯示評論</string>
|
||||
<string name="settings_eh_show_tag_translations">顯示標籤翻譯</string>
|
||||
<string name="settings_eh_show_tag_translations_summary">顯示翻譯後的標籤而非原始文字(需花費時間來下載資料檔案)</string>
|
||||
<string name="settings_eh_tag_translations_source">補充翻譯(由 EhTagTranslation 提供)</string>
|
||||
<string name="settings_eh_tag_translations_source_url">https://github.com/EhTagTranslation/Editor/wiki</string>
|
||||
<string name="settings_eh_filter">遮蔽圖庫</string>
|
||||
<string name="settings_eh_filter_summary">根據標題、上傳者、標籤遮蔽圖庫</string>
|
||||
<string name="settings_eh_filter">隱藏圖庫</string>
|
||||
<string name="settings_eh_filter_summary">根據標題、上傳者、標籤隱藏圖庫</string>
|
||||
|
||||
<string name="settings_read_volume_page">使用音量鍵翻頁</string>
|
||||
<string name="settings_read_reverse_volume">翻轉音量鍵</string>
|
||||
@@ -456,6 +467,7 @@
|
||||
<string name="proxy_invalid_port">無效的通訊埠</string>
|
||||
|
||||
<string name="cant_read_the_file">無法讀取檔案</string>
|
||||
<string name="db_version_too_high">資料庫版本過高</string>
|
||||
|
||||
<string name="app_language_system">系統語言(預設)</string>
|
||||
|
||||
@@ -513,7 +525,12 @@
|
||||
<string name="download_archive_started">開始下載壓縮檔</string>
|
||||
<string name="download_archive_failure">下載失敗</string>
|
||||
<string name="archive">壓縮檔</string>
|
||||
<string name="archive_free">免費</string>
|
||||
<string name="archive_original">原始檔案</string>
|
||||
<string name="archive_resample">重新採樣檔案</string>
|
||||
<string name="download_archive_failure_no_hath">下載壓縮檔需要 H@H 客戶端</string>
|
||||
<string name="current_funds">資金:%s GP,%d Credits</string>
|
||||
<string name="insufficient_funds">餘額不足</string>
|
||||
<string name="settings_privacy">隱私</string>
|
||||
<string name="settings_privacy_secure">不允許螢幕擷取</string>
|
||||
<string name="settings_privacy_secure_summary">啟用後,將無法對本程式進行螢幕擷取,同時,將不會在系統任務切換器中顯示該程式的預覽內容,重新啟動本程式以套用此變更</string>
|
||||
@@ -549,6 +566,7 @@
|
||||
<string name="metered_network_warning">正在使用流量計費網路</string>
|
||||
<string name="read_from">從第 %d 頁閱讀</string>
|
||||
<string name="settings_eh_request_news">啟動時請求新聞頁面</string>
|
||||
<string name="settings_eh_hide_hv_events">隱藏 HV 事件通知</string>
|
||||
<string name="copy_trans">複製翻譯</string>
|
||||
<string name="reset_download_location">恢復預設路徑</string>
|
||||
<string name="pick_new_download_location">選擇新路徑</string>
|
||||
@@ -576,28 +594,18 @@
|
||||
<string name="action_settings">設定</string>
|
||||
<string name="action_retry">重試</string>
|
||||
<string name="action_share">分享</string>
|
||||
<string name="action_copy">複製</string>
|
||||
<string name="action_save">儲存</string>
|
||||
<string name="action_save_to">儲存到…</string>
|
||||
<string name="pref_category_general">一般</string>
|
||||
<string name="pref_fullscreen">全螢幕</string>
|
||||
<string name="pref_page_transitions">翻頁轉場動畫</string>
|
||||
<string name="pref_show_page_number">顯示頁碼</string>
|
||||
<string name="pref_show_reader_seekbar">顯示跳頁拖動條</string>
|
||||
<string name="pref_crop_borders">邊緣裁剪</string>
|
||||
<string name="pref_custom_brightness">自訂亮度</string>
|
||||
<string name="pref_custom_color_filter">色彩濾鏡</string>
|
||||
<string name="pref_keep_screen_on">保持螢幕恆亮</string>
|
||||
<string name="pref_rotation_type">預設螢幕方向</string>
|
||||
<string name="rotation_free">自動</string>
|
||||
<string name="rotation_force_portrait">鎖定直向</string>
|
||||
<string name="rotation_force_landscape">鎖定橫向</string>
|
||||
<string name="color_filter_r_value">R</string>
|
||||
<string name="color_filter_g_value">G</string>
|
||||
<string name="color_filter_b_value">B</string>
|
||||
<string name="color_filter_a_value">A</string>
|
||||
<string name="pref_page_transitions">翻頁轉場動畫</string>
|
||||
<string name="pref_crop_borders">邊緣裁剪</string>
|
||||
<string name="pref_color_filter_mode">濾鏡融合機制</string>
|
||||
<string name="filter_mode_multiply">色彩增值</string>
|
||||
<string name="filter_mode_screen">濾色</string>
|
||||
<string name="pref_read_with_long_tap">長按顯示</string>
|
||||
<string name="pref_reader_theme">背景顏色</string>
|
||||
<string name="white_background">白色</string>
|
||||
<string name="black_background">黑色</string>
|
||||
@@ -618,39 +626,51 @@
|
||||
<string name="zoom_start_left">左邊</string>
|
||||
<string name="zoom_start_right">右邊</string>
|
||||
<string name="zoom_start_center">中間</string>
|
||||
<string name="decode_image_error">無法載入該圖片</string>
|
||||
<string name="pref_rotation_type">預設螢幕方向</string>
|
||||
<string name="rotation_free">自動</string>
|
||||
<string name="rotation_force_portrait">鎖定直向</string>
|
||||
<string name="rotation_force_landscape">鎖定橫向</string>
|
||||
<string name="color_filter_r_value">R</string>
|
||||
<string name="color_filter_g_value">G</string>
|
||||
<string name="color_filter_b_value">B</string>
|
||||
<string name="color_filter_a_value">A</string>
|
||||
<string name="custom_filter">濾鏡</string>
|
||||
<string name="gray_background">灰色</string>
|
||||
<string name="decode_image_error">無法載入該圖片</string>
|
||||
<string name="pref_read_with_long_tap">長按顯示</string>
|
||||
<string name="pref_color_filter_mode">濾鏡融合機制</string>
|
||||
<string name="filter_mode_multiply">色彩增值</string>
|
||||
<string name="filter_mode_screen">濾色</string>
|
||||
<string name="pref_cutout_short">使用螢幕凹口區域</string>
|
||||
<string name="action_menu">選單</string>
|
||||
<string name="webtoon_side_padding_25">25%</string>
|
||||
<string name="webtoon_side_padding_20">20%</string>
|
||||
<string name="webtoon_side_padding_15">15%</string>
|
||||
<string name="webtoon_side_padding_10">10%</string>
|
||||
<string name="webtoon_side_padding_0">無</string>
|
||||
<string name="action_menu">選單</string>
|
||||
<string name="pref_cutout_short">使用螢幕凹口區域</string>
|
||||
<string name="pref_webtoon_side_padding">頁緣留白</string>
|
||||
<string name="vertical_plus_viewer">垂直連貫</string>
|
||||
<string name="gray_background">灰色</string>
|
||||
<string name="viewer">閱讀模式</string>
|
||||
<string name="tapping_inverted_both">全部</string>
|
||||
<string name="tapping_inverted_both">水平 + 垂直</string>
|
||||
<string name="tapping_inverted_vertical">垂直</string>
|
||||
<string name="tapping_inverted_horizontal">水平</string>
|
||||
<string name="pref_read_with_tapping_inverted">反轉輕觸區域</string>
|
||||
<string name="vertical_plus_viewer">垂直連貫</string>
|
||||
<string name="tapping_inverted_none">無</string>
|
||||
<string name="pref_category_reading_mode">閱讀模式</string>
|
||||
<string name="pref_read_with_tapping_inverted">反轉輕觸區域</string>
|
||||
<string name="pref_viewer_nav">輕觸區域</string>
|
||||
<string name="edge_nav">邊緣式</string>
|
||||
<string name="kindlish_nav">Kindle 式</string>
|
||||
<string name="l_nav">L 式</string>
|
||||
<string name="pref_viewer_nav">輕觸區域</string>
|
||||
<string name="right_and_left_nav">左右式</string>
|
||||
<string name="nav_zone_right">右邊</string>
|
||||
<string name="nav_zone_left">左邊</string>
|
||||
<string name="nav_zone_next">下一頁</string>
|
||||
<string name="nav_zone_prev">上一頁</string>
|
||||
<string name="rotation_landscape">橫向</string>
|
||||
<string name="rotation_portrait">直向</string>
|
||||
<string name="rotation_type">螢幕方向</string>
|
||||
<string name="nav_zone_left">左邊</string>
|
||||
<string name="nav_zone_prev">上一頁</string>
|
||||
<string name="nav_zone_next">下一頁</string>
|
||||
<string name="nav_zone_right">右邊</string>
|
||||
<string name="rotation_landscape">橫向</string>
|
||||
<string name="automatic_background">自動</string>
|
||||
<string name="pref_grayscale">灰階</string>
|
||||
<string name="automatic_background">自動</string>
|
||||
<string name="pref_inverted_colors">反轉</string>
|
||||
<string name="label_default">預設</string>
|
||||
<string name="webtoon_side_padding_5">5%</string>
|
||||
@@ -658,5 +678,6 @@
|
||||
<string name="pref_navigate_pan">輕觸時移動圖片</string>
|
||||
<string name="rotation_reverse_portrait">顛倒直向</string>
|
||||
<string name="disabled_nav">停用</string>
|
||||
<string name="wide_color_gamut">使用 Display P3 色彩空間</string>
|
||||
<!-- Tachiyomi Reader End-->
|
||||
</resources>
|
||||
@@ -72,7 +72,8 @@
|
||||
<item>2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="search_min_rating">
|
||||
<string-array name="search_min_rating" translatable="false">
|
||||
<item>@string/unlimited</item>
|
||||
<item>@string/star_2</item>
|
||||
<item>@string/star_3</item>
|
||||
<item>@string/star_4</item>
|
||||
@@ -194,6 +195,7 @@
|
||||
<item>@string/app_language_zh_cn</item>
|
||||
<item>@string/app_language_zh_hk</item>
|
||||
<item>@string/app_language_zh_tw</item>
|
||||
<item>@string/app_language_nb_no</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="app_language_entry_values">
|
||||
@@ -208,6 +210,7 @@
|
||||
<item>zh-CN</item>
|
||||
<item>zh-HK</item>
|
||||
<item>zh-TW</item>
|
||||
<item>nb-NO</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="tag_translation_metadata" tools:ignore="InconsistentArrays">
|
||||
|
||||
Executable → Regular
+30
-91
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,17 +13,11 @@
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer</string>
|
||||
|
||||
--><resources>
|
||||
<string name="app_name" translatable="false">EhViewer</string>
|
||||
<string name="site_e" translatable="false">e-hentai</string>
|
||||
<string name="site_ex" translatable="false">exhentai</string>
|
||||
|
||||
<!-- Theme -->
|
||||
|
||||
<!-- Category -->
|
||||
<string name="doujinshi" translatable="false">DOUJINSHI</string>
|
||||
<string name="manga" translatable="false">MANGA</string>
|
||||
@@ -34,7 +29,6 @@
|
||||
<string name="cosplay" translatable="false">COSPLAY</string>
|
||||
<string name="asian_porn" translatable="false">ASIAN PORN</string>
|
||||
<string name="misc" translatable="false">MISC</string>
|
||||
|
||||
<string name="homepage">Homepage</string>
|
||||
<string name="subscription">Subscription</string>
|
||||
<string name="whats_hot">What\'s hot</string>
|
||||
@@ -43,26 +37,20 @@
|
||||
<string name="downloads">Downloads</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="stub" translatable="false">Stub</string>
|
||||
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
|
||||
<string name="sign_in">Sign in</string>
|
||||
<string name="register">Register</string>
|
||||
<string name="sign_in_via_webview">Sign in via WebView</string>
|
||||
<string name="sign_in_via_cookies">Sign in via cookies</string>
|
||||
<string name="sign_in_first">Please sign in first</string>
|
||||
|
||||
<string name="cookie_explain">A cookie is a small piece of data stored in web browser. Google it if you don\'t know what it is or how to get it.\n\nPlease enter special cookies to sign in.</string>
|
||||
<string name="text_is_empty">Text is empty</string>
|
||||
<string name="waring">Warning</string>
|
||||
<string name="wrong_cookie_warning">It seems that the cookies you entered are wrong. You\'d better check them.</string>
|
||||
|
||||
<string name="invalid_download_location">It seems download location is not available. Please set it in Settings.</string>
|
||||
|
||||
<string name="clipboard_gallery_url_snack_message">There is a gallery url in the clipboard</string>
|
||||
<string name="clipboard_gallery_url_snack_message">There is a gallery URL in the clipboard</string>
|
||||
<string name="clipboard_gallery_url_snack_action">View</string>
|
||||
|
||||
<string name="error_status_code_400" translatable="false">Bad Request</string>
|
||||
<string name="error_status_code_401" translatable="false">Unauthorized</string>
|
||||
<string name="error_status_code_402" translatable="false">Payment Required</string>
|
||||
@@ -81,14 +69,12 @@
|
||||
<string name="error_status_code_415" translatable="false">Unsupported Media Type</string>
|
||||
<string name="error_status_code_416" translatable="false">Requested Range Not Satisfiable</string>
|
||||
<string name="error_status_code_417" translatable="false">Expectation Failed</string>
|
||||
|
||||
<string name="error_status_code_500" translatable="false">Internal Server Error</string>
|
||||
<string name="error_status_code_501" translatable="false">Not Implemented</string>
|
||||
<string name="error_status_code_502" translatable="false">Bad Gateway</string>
|
||||
<string name="error_status_code_503" translatable="false">Service Unavailable</string>
|
||||
<string name="error_status_code_504" translatable="false">Gateway Timeout</string>
|
||||
<string name="error_status_code_505" translatable="false">HTTP Version Not Supported</string>
|
||||
|
||||
<string name="error_bad_status_code">Bad status code: %d</string>
|
||||
<string name="error_timeout">Timeout</string>
|
||||
<string name="error_unknown_host">Unknown host</string>
|
||||
@@ -96,60 +82,50 @@
|
||||
<string name="error_socket">Network error</string>
|
||||
<string name="error_unknown">Weird</string>
|
||||
<string name="error_cant_find_activity">Can\'t find the application</string>
|
||||
<string name="error_cannot_parse_the_url">Can\'t parse the url</string>
|
||||
<string name="error_cannot_parse_the_url">Can\'t parse the URL</string>
|
||||
<string name="error_decoding_failed">Decoding failed</string>
|
||||
<string name="error_reading_failed">Reading Failed</string>
|
||||
<string name="error_out_of_range">Out of range</string>
|
||||
<string name="error_write_failed">Write failed</string>
|
||||
<string name="error_parse_error">Parse error</string>
|
||||
<string name="error_509">509</string>
|
||||
<string name="error_invalid_url">Invalid url</string>
|
||||
<string name="error_invalid_url">Invalid URL</string>
|
||||
<string name="error_get_ptoken_error">Get pToken error</string>
|
||||
<string name="error_cant_create_temp_file">Couldn\'t create temp file</string>
|
||||
<string name="error_cant_save_image">Can\'t save image</string>
|
||||
<string name="error_invalid_number">Invalid number</string>
|
||||
<string name="error_please_login_first">Please login first</string>
|
||||
<string name="error_please_login_first">Please log in first</string>
|
||||
<string name="error_empty_html">Empty page</string>
|
||||
|
||||
<string name="kokomade_tip">Settings->Eh->Gallery Site->e-hentai</string>
|
||||
|
||||
<string name="kokomade_tip">Settings->Eh->Gallery Site->e-hentai</string>
|
||||
<string name="app_waring">The content of this application is from the Internet. Some of it may do physical or mental harm to you. You have learnt the risks above and would like to undertake them. By continuing to use it, you agree to the above terms.</string>
|
||||
<string name="error_username_cannot_empty">Username cannot be an empty string</string>
|
||||
<string name="error_password_cannot_empty">Password cannot be an empty string</string>
|
||||
<string name="tourist_mode">Continue without signing in</string>
|
||||
<string name="sign_in_failed">Sign in failed</string>
|
||||
<string name="sign_in_failed_tip">If this issue continues, try \"Sign in via WebView\" or \"Sign in via cookies\"</string>
|
||||
<string name="sign_in_failed_tip">If this issue continues, try \"Sign in via WebView\" or \"Sign in via cookies\".</string>
|
||||
<string name="get_it">Got it</string>
|
||||
|
||||
<!-- Select scene -->
|
||||
<string name="select_scene">Which gallery site do you want to visit?</string>
|
||||
<string name="select_scene_explain">e-hentai: available for anyone\nexhentai: available for who has signed in</string>
|
||||
|
||||
<!-- Update -->
|
||||
|
||||
<!-- Gallery List -->
|
||||
<string name="gallery_list_search_bar_hint_exhentai">Search ExHentai</string>
|
||||
<string name="gallery_list_search_bar_hint_e_hentai">Search E-Hentai</string>
|
||||
<string name="gallery_list_search_bar_open_gallery">Open the gallery</string>
|
||||
<string name="gallery_list_empty_hit">The World is Big and the panda sit alone</string>
|
||||
<string name="gallery_list_empty_hit_subscription">Subscribe to tags in Settings->EH->My tags</string>
|
||||
|
||||
<string name="gallery_list_empty_hit_subscription">Subscribe to tags in Settings->EH->My tags</string>
|
||||
<string name="search">Search</string>
|
||||
|
||||
<string name="keyword_search">Keyword search</string>
|
||||
<string name="image_search">Image search</string>
|
||||
|
||||
<string name="search_normal">Normal Search</string>
|
||||
<string name="search_advance">Advance Options</string>
|
||||
<string name="search_advance">Advanced Options</string>
|
||||
<string name="search_image">Image Search</string>
|
||||
<string name="search_normal_search">Normal search</string>
|
||||
<string name="search_subscription_search">Subscription search</string>
|
||||
<string name="search_specify_uploader">Specify uploader</string>
|
||||
<string name="search_specify_tag">Specify tag</string>
|
||||
<string name="search_enable_advance">Enable advance options</string>
|
||||
|
||||
<string name="search_tip">Normal search: Simply search.\n\nSubscription search: Search in subscriptions.\n\nSpecify uploader: Search for galleries uploaded by the specified uploader. All other options will be ignored.\n\nSpecify tag: Search for galleries that contain the specified tag. Only one tag accepted. Other options will be ignored.</string>
|
||||
|
||||
<string name="search_sh">Only Show Expunged Galleries</string>
|
||||
<string name="search_sto">Only Show Galleries With Torrents</string>
|
||||
<string name="search_sr">Minimum Rating:</string>
|
||||
@@ -158,22 +134,18 @@
|
||||
<string name="search_sp_err1">The page range maximum cannot be below 10</string>
|
||||
<string name="search_sp_err2">The page range must be at least 20</string>
|
||||
<!-- A placeholder for other languages -->
|
||||
<string name="search_sp_suffix" />
|
||||
<string name="search_sp_suffix"/>
|
||||
<string name="search_sf">Disable default filters for:</string>
|
||||
<string name="search_sfl">Language</string>
|
||||
<string name="search_sfu">Uploader</string>
|
||||
<string name="search_sft">Tags</string>
|
||||
|
||||
<string name="select_image">Select image</string>
|
||||
<string name="search_uss">Use similarity scan</string>
|
||||
<string name="search_osc">Only search covers</string>
|
||||
<string name="select_image_first">Please select image first</string>
|
||||
|
||||
<string name="add_to_favourites">Add to favourites</string>
|
||||
<string name="remove_from_favourites">Remove from favourites</string>
|
||||
|
||||
<string name="delete_downloads">Delete downloads</string>
|
||||
|
||||
<string name="quick_search">Quick search</string>
|
||||
<string name="quick_search_tip">Tap \"+\" to add Quick Search</string>
|
||||
<string name="add_quick_search_dialog_title">Add Quick Search</string>
|
||||
@@ -186,15 +158,13 @@
|
||||
<string name="duplicate_name">This name is already in use.</string>
|
||||
<string name="save_progress">Save progress</string>
|
||||
<string name="delete_quick_search">Delete quick search \"%s\"?</string>
|
||||
|
||||
<string name="go_to_hint">Page %1$d, total %2$d pages</string>
|
||||
|
||||
<!-- search_min_rating -->
|
||||
<string name="unlimited">Unlimited</string>
|
||||
<string name="star_2">2 stars</string>
|
||||
<string name="star_3">3 stars</string>
|
||||
<string name="star_4">4 stars</string>
|
||||
<string name="star_5">5 stars</string>
|
||||
|
||||
<!-- Gallery detail -->
|
||||
<string name="error_cannot_find_gallery">Couldn\'t find the gallery</string>
|
||||
<string name="download">Download</string>
|
||||
@@ -238,7 +208,6 @@
|
||||
<string name="filter_added">Blocker added</string>
|
||||
<string name="newer_version_avaliable">There are newer versions of this gallery available.</string>
|
||||
<string name="newer_version_title">%1$s, added %2$s</string>
|
||||
|
||||
<!-- Rating -->
|
||||
<string name="rating10">MASTERPIECE</string>
|
||||
<string name="rating9">AMAZING</string>
|
||||
@@ -252,15 +221,14 @@
|
||||
<string name="rating1">UNBEARABLE</string>
|
||||
<string name="rating0">DISASTER</string>
|
||||
<string name="rating_none" translatable="false">(´_ゝ`)</string>
|
||||
|
||||
<!-- Gallery Info -->
|
||||
<string name="gallery_info">Gallery Info</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="header_key">Key</string>
|
||||
<string name="header_value">Value</string>
|
||||
<string name="key_gid">Gid</string>
|
||||
<string name="key_gid">GID</string>
|
||||
<string name="key_token">Token</string>
|
||||
<string name="key_url">Url</string>
|
||||
<string name="key_url">URL</string>
|
||||
<string name="key_title">Title</string>
|
||||
<string name="key_title_jpn">Jpn Title</string>
|
||||
<string name="key_thumb">Thumb</string>
|
||||
@@ -277,8 +245,7 @@
|
||||
<string name="key_rating_count">Rating count</string>
|
||||
<string name="key_rating">Rating</string>
|
||||
<string name="key_torrents">Torrents</string>
|
||||
<string name="key_torrent_url">Torrent Url</string>
|
||||
|
||||
<string name="key_torrent_url">Torrent URL</string>
|
||||
<!-- Gallery Comments -->
|
||||
<string name="gallery_comments">Gallery Comments</string>
|
||||
<string name="no_one_comments_gallery">No comment.</string>
|
||||
@@ -302,11 +269,9 @@
|
||||
<string name="check_vote_status">View vote details</string>
|
||||
<string name="click_more_comments">Click to load more comments</string>
|
||||
<string name="last_edited">Last edited: %s</string>
|
||||
|
||||
<!-- Gallery Previews -->
|
||||
<string name="gallery_previews">Gallery Previews</string>
|
||||
<string name="go_to">Go to</string>
|
||||
|
||||
<!-- Download -->
|
||||
<string name="scene_download_title">Download - %s</string>
|
||||
<string name="no_download_info">Download items will be shown here</string>
|
||||
@@ -343,7 +308,6 @@
|
||||
<string name="remember_download_label">Remember download label</string>
|
||||
<string name="default_download_label">Default download label</string>
|
||||
<string name="added_to_download_list">Added to download list</string>
|
||||
|
||||
<!-- Download label -->
|
||||
<string name="add">Add</string>
|
||||
<string name="new_label_title">New label</string>
|
||||
@@ -352,12 +316,10 @@
|
||||
<string name="label_text_exist">Label exists</string>
|
||||
<string name="rename_label_title">Rename label</string>
|
||||
<string name="delete_label">Delete label \"%s\"?</string>
|
||||
|
||||
<!-- History -->
|
||||
<string name="no_history">Viewed galleries will be shown here</string>
|
||||
<string name="clear_all">Clear all</string>
|
||||
<string name="clear_all_history">Clear all history?</string>
|
||||
|
||||
<!-- Filter -->
|
||||
<string name="filter">Blockers</string>
|
||||
<string name="filter_title">Title</string>
|
||||
@@ -372,21 +334,17 @@
|
||||
<string name="show_definition">Show definition</string>
|
||||
<string name="filter_text">Blocker text</string>
|
||||
<string name="filter_tip">This blocking system filters the result of EHentai website blocking system.\n\nTitle Blocker: exclude the gallery whose title contains the word.\n\nUploader Blocker: exclude the gallery which was uploaded by the uploader.\n\nTag Blocker: exclude the gallery which contain the tag, it takes more time to get gallery list.\n\nTag Namespace Blocker: exclude the gallery which contain the tag namespace, it takes more time to get gallery list.\n\nCommenter Blocker: exclude the comments posted by the commenter.\n\nComment Blocker: exclude the comments matching the regex.</string>
|
||||
|
||||
<!-- Set Security -->
|
||||
|
||||
<!-- UConfig Activity -->
|
||||
<string name="u_config">EHentai settings</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="apply_tip">Tap the check mark to save the settings</string>
|
||||
|
||||
<!-- My Tags Activity -->
|
||||
<string name="my_tags">My tags</string>
|
||||
|
||||
<!-- Hosts Activity -->
|
||||
<string name="hosts">Custom hosts.txt</string>
|
||||
<string name="hosts_host">Host</string>
|
||||
<string name="hosts_ip">IP1(+IP2+IP3…)</string>
|
||||
<string name="hosts_ip">IP1(+IP2+IP3…)</string>
|
||||
<string name="add_host">Add a host</string>
|
||||
<string name="add_host_add">Add</string>
|
||||
<string name="edit_host">Edit the host</string>
|
||||
@@ -394,11 +352,9 @@
|
||||
<string name="edit_host_delete">Delete</string>
|
||||
<string name="invalid_host">Invalid host</string>
|
||||
<string name="invalid_ip">Invalid IP</string>
|
||||
|
||||
<!-- Gallery Activity -->
|
||||
<string name="share_image">Share image</string>
|
||||
<string name="image_saved">Image saved to %s</string>
|
||||
|
||||
<string name="settings_eh">EH</string>
|
||||
<string name="settings_eh_sign_out">Sign out</string>
|
||||
<string name="settings_eh_sign_out_tip">You have signed out. Restart the application to sign in again</string>
|
||||
@@ -431,14 +387,10 @@
|
||||
<string name="settings_eh_show_tag_translations_summary">Show tag translations instead of the original text (It takes time to download the data file)</string>
|
||||
<string name="settings_eh_tag_translations_source">Placeholder</string>
|
||||
<string name="settings_eh_tag_translations_source_url">https://placeholder</string>
|
||||
<string name="settings_eh_preview_num">The maximum number of previews on the gallery details page</string>
|
||||
<string name="settings_eh_preview_num_summary">For historical reasons, large value could cause lag</string>
|
||||
<string name="settings_eh_filter">Blockers</string>
|
||||
<string name="settings_eh_filter_summary">Block gallery or comment by title, uploader, tags and commenter</string>
|
||||
|
||||
<string name="settings_read_volume_page">Use volume key to turn pages</string>
|
||||
<string name="settings_read_reverse_volume">Reverse volume key</string>
|
||||
|
||||
<string name="settings_download">Download</string>
|
||||
<string name="settings_download_download_location">Download location</string>
|
||||
<string name="settings_download_invalid_download_location">Invalid download location</string>
|
||||
@@ -466,9 +418,8 @@
|
||||
<string name="settings_download_clean_redundancy_summary">Remove gallery images which are not in download list but in download location</string>
|
||||
<string name="settings_download_clean_redundancy_no_redundancy">No redundancy</string>
|
||||
<string name="settings_download_clean_redundancy_done">Redundancy cleaning completed, clean-up %d items totally</string>
|
||||
|
||||
<string name="settings_advanced">Advanced</string>
|
||||
<string name="settings_advanced_save_parse_error_body">Save html content when parsing error</string>
|
||||
<string name="settings_advanced_save_parse_error_body">Save HTML content when parsing error</string>
|
||||
<string name="settings_advanced_save_parse_error_body_summary">Html content may be privacy-sensitive</string>
|
||||
<string name="settings_advanced_save_crash_log">Save crash log when app crashes</string>
|
||||
<string name="settings_advanced_save_crash_log_summary">Crash logs help developers fix bugs</string>
|
||||
@@ -485,6 +436,8 @@
|
||||
<string name="settings_advanced_built_in_hosts_summary">Apply the built-in host-to-IP mapping provided by the app.\nIt can be overridden by custom hosts.txt.</string>
|
||||
<string name="settings_advanced_domain_fronting_title">Domain fronting</string>
|
||||
<string name="settings_advanced_domain_fronting_summary">Bypass SNI blocking.</string>
|
||||
<string name="settings_advanced_bypass_vpn_title">Bypass VPN</string>
|
||||
<string name="settings_advanced_bypass_vpn_summary">Try to bypass VPN when domain fronting is enabled</string>
|
||||
<string name="settings_advanced_custom_hosts_title">Custom hosts.txt</string>
|
||||
<string name="settings_advanced_custom_hosts_summary">Map hostnames to IP addresses.\nIt can override built-in hosts.txt.</string>
|
||||
<string name="settings_advanced_export_data">Export data</string>
|
||||
@@ -493,14 +446,13 @@
|
||||
<string name="settings_advanced_export_data_failed">Failed to export data</string>
|
||||
<string name="settings_advanced_import_data">Import data</string>
|
||||
<string name="settings_advanced_import_data_summary">Load data which were previously saved</string>
|
||||
<string name="settings_advanced_import_data_successfully">Data imported successfully</string>
|
||||
<string name="settings_advanced_import_data_successfully">Data imported</string>
|
||||
<string name="settings_advanced_backup_favorite">Backup favorite list</string>
|
||||
<string name="settings_advanced_backup_favorite_summary">Backup remote favorite list to local</string>
|
||||
<string name="settings_advanced_backup_favorite_start">Backing up favorite list %s</string>
|
||||
<string name="settings_advanced_backup_favorite_nothing">Nothing to backup</string>
|
||||
<string name="settings_advanced_backup_favorite_success">Backup favorite list success</string>
|
||||
<string name="settings_advanced_backup_favorite_failed">Backup favorite list failed</string>
|
||||
|
||||
<string name="settings_about">About</string>
|
||||
<string name="settings_about_declaration" translatable="false">EhViewer</string>
|
||||
<string name="settings_about_declaration_summary">EhViewer is not affiliated with E-Hentai.org in any way</string>
|
||||
@@ -510,18 +462,15 @@
|
||||
<string name="settings_about_source">Source</string>
|
||||
<string name="settings_about_version">Build version</string>
|
||||
<string name="settings_about_check_for_updates">Check for updates</string>
|
||||
|
||||
<string name="proxy_direct">Direct</string>
|
||||
<string name="proxy_system">System Proxy</string>
|
||||
<string name="proxy_host_or_ip">Host or IP</string>
|
||||
<string name="proxy_port">Port</string>
|
||||
<string name="proxy_invalid_port">Invalid port</string>
|
||||
|
||||
<string name="cant_read_the_file">Can\'t read the file</string>
|
||||
<string name="db_version_too_high">Database version too high</string>
|
||||
|
||||
<string name="app_language_system">System Language (Default)</string>
|
||||
<string name="app_language_de">Deutsch</string>
|
||||
<string name="app_language_de" translatable="false">Deutsch</string>
|
||||
<string name="app_language_en" translatable="false">English</string>
|
||||
<string name="app_language_es" translatable="false">Español</string>
|
||||
<string name="app_language_fr" translatable="false">Français</string>
|
||||
@@ -531,15 +480,11 @@
|
||||
<string name="app_language_zh_cn" translatable="false">中文(大陆)</string>
|
||||
<string name="app_language_zh_hk" translatable="false">中文(香港)</string>
|
||||
<string name="app_language_zh_tw" translatable="false">國文(臺灣)</string>
|
||||
|
||||
<string name="app_language_nb_no" translatable="false">Norwegian Bokmål</string>
|
||||
<!-- ExcludedLanguagesScene -->
|
||||
|
||||
<string name="please_wait">Pleas wait</string>
|
||||
|
||||
<string name="please_wait">Please wait</string>
|
||||
<!-- License -->
|
||||
|
||||
<!-- Dir Picker-->
|
||||
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">Cloud Favorites</string>
|
||||
<string name="local_favorites">Local Favorites</string>
|
||||
@@ -553,12 +498,9 @@
|
||||
<string name="let_me_select">Let me select</string>
|
||||
<string name="collections">Collections</string>
|
||||
<string name="need_sign_in">Signing in is required</string>
|
||||
|
||||
<!-- Progress Scene -->
|
||||
<string name="error_something_wrong_happened">Something wrong happened</string>
|
||||
|
||||
<!-- Guide -->
|
||||
|
||||
<!-- Readable Time -->
|
||||
<string name="from_the_future">From the future</string>
|
||||
<string name="just_now">Just now</string>
|
||||
@@ -600,18 +542,17 @@
|
||||
<string name="archive_original">Original archive</string>
|
||||
<string name="archive_resample">Resample archive</string>
|
||||
<string name="download_archive_failure_no_hath">Need H@H client for archive download</string>
|
||||
<string name="current_funds">Funds: %s GP, %d Credits</string>
|
||||
<string name="insufficient_funds">Insufficient Funds</string>
|
||||
<string name="settings_privacy">Privacy</string>
|
||||
<string name="settings_privacy_secure">Prevent screenshots</string>
|
||||
<string name="settings_privacy_secure_summary">Prevent the content of the app from being taken screenshots of or shown in the \"Recent Apps\" list.</string>
|
||||
|
||||
<string name="download_service">Download Service</string>
|
||||
<string name="favorite_name">Favorite</string>
|
||||
|
||||
<string name="black_dark_theme">Black dark theme</string>
|
||||
<string name="download_filter">Filter</string>
|
||||
<string name="download_all">All</string>
|
||||
<string name="download_start_all_reversed">Start all (reversed)</string>
|
||||
|
||||
<string name="no_browser_installed">Just install a browser please.</string>
|
||||
<string name="toplist_alltime">All-Time</string>
|
||||
<string name="toplist_pastyear">Past Year</string>
|
||||
@@ -640,21 +581,18 @@
|
||||
<string name="app_link_not_verified_message">For Android 12 and newer, you need to manually add link to verified links in order to open E-Hentai links in EhViewer.</string>
|
||||
<string name="app_link_not_verified_title">App links not verified</string>
|
||||
<string name="open_by_default">Open by default</string>
|
||||
|
||||
<string name="settings_privacy_require_unlock">Require Unlock</string>
|
||||
<string name="settings_privacy_require_unlock_delay">Lock Delay</string>
|
||||
<string name="settings_privacy_require_unlock_delay_summary">No unlock is required when leaving App and returning within %s minute(s)</string>
|
||||
<string name="settings_privacy_require_unlock_delay_summary_immediately">Unlock is required whenever you return to this App</string>
|
||||
<string name="proxy_type_name">Proxy Type</string>
|
||||
<string name="filter_label">Blocker Type</string>
|
||||
|
||||
<string name="archive_passwd">password</string>
|
||||
<string name="archive_need_passwd">Archive need password</string>
|
||||
<string name="passwd_wrong">Password Wrong</string>
|
||||
<string name="passwd_cannot_be_empty">Password can\'t be empty</string>
|
||||
<string name="list_tile_thumb_size">Thumb size in thumb mode</string>
|
||||
<string name="account_name">Account</string>
|
||||
|
||||
<!-- Tachiyomi Reader Start-->
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="pref_rotation_type">Default rotation type</string>
|
||||
@@ -684,6 +622,7 @@
|
||||
<string name="pref_cutout_short">Show content in cutout area</string>
|
||||
<string name="pref_page_transitions">Animate page transitions</string>
|
||||
<string name="pref_show_page_number">Show page number</string>
|
||||
<string name="pref_show_reader_seekbar">Show page jumping seekbar</string>
|
||||
<string name="pref_custom_brightness">Custom brightness</string>
|
||||
<string name="pref_grayscale">Grayscale</string>
|
||||
<string name="pref_inverted_colors">Inverted</string>
|
||||
@@ -702,7 +641,7 @@
|
||||
<string name="white_background">White</string>
|
||||
<string name="gray_background">Gray</string>
|
||||
<string name="black_background">Black</string>
|
||||
<string name="automatic_background">Automatic</string>
|
||||
<string name="automatic_background">Auto</string>
|
||||
<string name="l_nav">L shaped</string>
|
||||
<string name="kindlish_nav">Kindle-ish</string>
|
||||
<string name="edge_nav">Edge</string>
|
||||
@@ -741,7 +680,7 @@
|
||||
<string name="action_share">Share</string>
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_save">Save</string>
|
||||
<string name="action_save_to">Save to…</string>
|
||||
<string name="action_save_to">Save to…</string>
|
||||
<string name="wide_color_gamut">Use Display P3 color space</string>
|
||||
<!-- Tachiyomi Reader End-->
|
||||
</resources>
|
||||
@@ -43,8 +43,8 @@
|
||||
android:title="@string/settings_advanced_read_cache_size"
|
||||
app:entries="@array/read_cache_size_entries"
|
||||
app:entryValues="@array/read_cache_size_entry_values"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
app:iconSpaceReserved="false"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<rikka.preference.SimpleMenuPreference
|
||||
android:defaultValue="system"
|
||||
@@ -52,8 +52,8 @@
|
||||
android:title="@string/settings_advanced_app_language_title"
|
||||
app:entries="@array/app_language_entries"
|
||||
app:entryValues="@array/app_language_entry_values"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
app:iconSpaceReserved="false"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<com.hippo.ehviewer.preference.ProxyPreference
|
||||
android:key="proxy"
|
||||
@@ -80,6 +80,13 @@
|
||||
android:title="@string/settings_advanced_domain_fronting_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<rikka.material.preference.MaterialSwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="bypass_vpn"
|
||||
android:summary="@string/settings_advanced_bypass_vpn_summary"
|
||||
android:title="@string/settings_advanced_bypass_vpn_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="export_data"
|
||||
android:summary="@string/settings_advanced_export_data_summary"
|
||||
|
||||
@@ -146,17 +146,6 @@
|
||||
app:iconSpaceReserved="false"
|
||||
app:url="@string/settings_eh_tag_translations_source_url" />
|
||||
|
||||
<SeekBarPreference
|
||||
android:defaultValue="60"
|
||||
android:key="preview_num"
|
||||
android:max="200"
|
||||
android:summary="@string/settings_eh_preview_num_summary"
|
||||
android:title="@string/settings_eh_preview_num"
|
||||
app:iconSpaceReserved="false"
|
||||
app:min="10"
|
||||
app:showSeekBarValue="true"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
<Preference
|
||||
android:key="filter"
|
||||
android:summary="@string/settings_eh_filter_summary"
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<locale android:name="zh-CN" />
|
||||
<locale android:name="zh-HK" />
|
||||
<locale android:name="zh-TW" />
|
||||
<locale android:name="nb-NO" />
|
||||
</locale-config>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.0-alpha11' apply false
|
||||
id 'com.android.application' version '8.0.0-beta01' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
||||
id 'com.google.devtools.ksp' version '1.8.0-1.0.8' apply false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user