Compare commits

..
Author SHA1 Message Date
tarsin d7fcda1f83 Release 1.8.5.3
Releases / Build (push) Has been cancelled
2023-01-23 09:26:25 +08:00
FooIbar b832c24185 Remove app:navGraph from xml
Since we set it through setGraph()
2023-01-23 08:16:20 +08:00
FooIbar 3b04f8c695 Try to bypass VPN when domain fronting is enabled
Note VPN may disallow bypass
2023-01-22 21:09:44 +08:00
tarsin 0ef11b58d2 Move route out of object 2023-01-22 16:27:36 +08:00
tarsin bf13bbf506 Tweak directory structure 2023-01-22 16:24:02 +08:00
tarsin 92c94aa22a Fix typo 2023-01-22 14:04:28 +08:00
tarsin 1c0785432b Avoid unnecessary domain fronting, #711 2023-01-22 13:47:12 +08:00
Allan Nordhøy dcb9a50061 Translated using Weblate (Norwegian Bokmål)
Currently translated at 46.0% (282 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/nb_NO/
2023-01-22 09:09:19 +08:00
FooIbar de7f4bdb6e Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/zh_Hans/
2023-01-22 09:09:19 +08:00
Allan Nordhøy 73d636dba6 Translated using Weblate (English)
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/
2023-01-22 09:09:19 +08:00
FooIbar 3f1c453ff6 Update strings.xml 2023-01-21 22:48:47 +08:00
tarsin ecb6baaebb Update arrays.xml 2023-01-21 10:03:59 +08:00
tarsin e1cba45486 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (609 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/zh_Hans/
2023-01-21 09:50:49 +08:00
Allan Nordhøy ea4181b243 Translated using Weblate (English)
Currently translated at 99.8% (612 of 613 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/
2023-01-21 09:50:49 +08:00
tarsin 6c5b9f8851 Add nb-rNO to supported languages 2023-01-21 09:10:57 +08:00
c1fccaf240 Translations update from Hosted Weblate (#701)
* Translated using Weblate (English)

Currently translated at 99.8% (610 of 611 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (English)

Currently translated at 99.8% (611 of 612 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/en/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 19.7% (121 of 612 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/nb_NO/

Co-authored-by: tarsin <yuanqingxiang233@163.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2023-01-21 08:32:21 +08:00
FooIbar e7c223da7d fixup! Resolve #604 2023-01-21 08:12:22 +08:00
FooIbar 6bfe1ba876 Resolve #703 2023-01-20 23:11:23 +08:00
FooIbar 574e5dab18 Remove minimum rating checkbox in advanced search 2023-01-20 21:23:30 +08:00
tarsin 5dc9b83831 Update README.md 2023-01-20 19:22:21 +08:00
FooIbar 57a214a9a4 Fix thumb url for Downloads/History/LocalFavorite 2023-01-20 18:58:29 +08:00
FooIbar cf48eb94af Convert EhUtils to kotlin 2023-01-20 18:58:29 +08:00
FooIbar 683f1e488a Rename .java to .kt 2023-01-20 18:58:29 +08:00
FooIbar e888ae4da7 Use different app name for debug variant 2023-01-20 18:58:29 +08:00
tarsin 4512f2a375 Bump Versio
Releases / Build (push) Has been cancelled
2023-01-20 18:07:19 +08:00
tarsin 68442afa0f Update coil 2023-01-20 12:31:46 +08:00
tarsin ddf4d0cbc3 Use our modified coil temporarily 2023-01-20 10:59:38 +08:00
FooIbar 4be185e428 Resolve #696 2023-01-19 21:30:24 +08:00
tarsin f75e66ac19 Upgrade compose compiler 2023-01-18 21:21:40 +08:00
tarsin abda781f02 Cleanup code 2023-01-18 18:50:38 +08:00
tarsin c1533ec66d Avoid unnecessary lambda object create 2023-01-18 18:23:46 +08:00
tarsin c8319446b0 Bump Version
Releases / Build (push) Has been cancelled
2023-01-18 17:44:45 +08:00
tarsin 7ae452cf13 Fixup! Use FileChannel transferFrom 2023-01-18 12:29:19 +08:00
tarsin 4c024c5faa Fixup! Use FileChannel transferFrom 2023-01-18 12:24:10 +08:00
tarsin 9d16e9d98c Use FileChannel transferFrom, Resolve #682 2023-01-18 12:18:43 +08:00
tarsin 48357d08f7 Add TODOs 2023-01-18 10:47:15 +08:00
tarsin 8e0bcf40db Rewrite the GIF frame delay if it's below a threshold, reland, Resolve #641 2023-01-18 10:46:21 +08:00
renovate[bot] 85ef5f04dd Update plugin com.android.application to v8.0.0-beta01 2023-01-18 09:08:12 +08:00
tarsin 18e0748d14 Revert "Update dependency gradle to v8.0-rc-2"
This reverts commit cdde5b098e.
2023-01-18 09:08:01 +08:00
renovate[bot] cdde5b098e Update dependency gradle to v8.0-rc-2 2023-01-17 21:19:03 +08:00
FooIbar f83964752d Set gallery site to eh when sign out 2023-01-17 16:49:16 +08:00
tarsin 0c3e34bbf9 Fix type warning 2023-01-17 10:50:56 +08:00
tarsin 6dea7faac8 Introduce gif rewriter stub 2023-01-17 10:50:11 +08:00
tarsin 726f482c33 Introduce new file read path 2023-01-17 10:40:20 +08:00
tarsin cb242f664a Convert Archive load to ByteBufferSource path 2023-01-17 09:29:44 +08:00
tarsin edc0345970 Introduce out image source type 2023-01-17 09:15:48 +08:00
tarsin a50535dd76 Fix crash 2023-01-17 09:00:10 +08:00
tarsin 505942aa25 Explicitly depend latest okio 2023-01-17 08:56:05 +08:00
tarsin 38a593a9de Resolve #686 2023-01-17 08:51:51 +08:00
tarsin da87fb2551 Fix image save 2023-01-17 08:20:57 +08:00
tarsin d4dc65a4ba Drop old disklrucache (#685)
* Drop Old DiskLruCache

* Avoid being interrupted when completing editor

* Protect all journal operations

* Revert "Protect all journal operations"

This reverts commit 5b7c7e3b33a515ee3e92826a6b76c2dfb3040c5d.

* Revert "Avoid being interrupted when completing editor"

This reverts commit cf4fbed1281eff766c6e65b6f685dfc355208a0b.

* Shutdown Thread gracefully

Test: m, Check no thread leaks

* Fix download extensions
2023-01-16 21:44:25 +08:00
Edward Wu 53463caedd Ehviewer: Translate: Update zh-rTW Translate (01/06)
* Update strings translate
* Reformat to be consistent with Simplified Chinese xml

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2023-01-16 18:34:37 +08:00
tarsin 0707a0fad8 Report DiskLruCache corruption if met 2023-01-16 17:42:55 +08:00
tarsin dd1024af27 Use Tonal Button for register 2023-01-16 17:38:24 +08:00
FooIbar 7edc58a6da Get funds from GP Exchange when unavailable from the archive page 2023-01-16 17:08:03 +08:00
FooIbar 5a5da0cace Convert EhUrl to kotlin 2023-01-16 17:08:03 +08:00
FooIbar 53f02ed366 Rename .java to .kt 2023-01-16 17:08:03 +08:00
tarsin 27cfa885e7 Resolve #681 2023-01-16 12:11:47 +08:00
tarsin beb132d604 Revert "Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)"
This reverts commit 0ebb55551c.
2023-01-16 11:18:06 +08:00
tarsin b1b10298b4 Revert "Avoid unnecessary overhead"
This reverts commit eada1bc023.
2023-01-16 11:17:53 +08:00
tarsin b1993ff7d2 Fix Gif LruCache size 2023-01-16 10:56:02 +08:00
FooIbar 2afe5fe5b2 Show current funds in archive dialog 2023-01-15 22:19:41 +08:00
FooIbar 52fa8f64cc Convert ArchiveParser to kotlin 2023-01-15 22:19:41 +08:00
FooIbar 4f7978070f Rename .java to .kt 2023-01-15 22:19:41 +08:00
tarsin a909983822 Don't exclude compose metadata 2023-01-15 16:45:43 +08:00
小白-白 5267cad896 Fix wrong extension when downloading 2023-01-15 14:34:03 +08:00
小白-白 ce274cf65b Hide unavailable archive 2023-01-15 14:34:03 +08:00
tarsin 55b6b2e34d Cleanup res 2023-01-15 13:29:18 +08:00
tarsin 5e4e2971b5 Fix code style 2023-01-15 13:24:09 +08:00
tarsin a933bcfb1e Resolve #677 2023-01-15 13:23:05 +08:00
tarsin b4b38e27f3 Resolve #675 2023-01-15 13:17:13 +08:00
tarsin bc70824b57 Fixup! Fix stuck when loading preview 2023-01-15 12:55:05 +08:00
tarsin 87ecaaa6a4 Fixup! Use viewLifecycleOwner 2023-01-15 12:52:21 +08:00
tarsin c4cd9cd802 Fix stuck when loading preview 2023-01-15 12:49:18 +08:00
tarsin bbe0e8c5a9 Remove preview limit
At least it works well on my MT6765
2023-01-15 12:42:04 +08:00
tarsin 3f8984d2dc Resolve #668 2023-01-15 10:50:55 +08:00
87 changed files with 1582 additions and 3267 deletions

No files matched your search

+6
View File
@@ -73,6 +73,12 @@ with Material Design 3 and Dynamic Color Support
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# 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
View File
@@ -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'
}
@@ -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>
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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)
+28 -1
View File
@@ -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);
}
-11
View File
@@ -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);
}
+44
View File
@@ -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)
}
+6 -2
View File
@@ -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 \\[[^]]*] &nbsp; ([\\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,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
@@ -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"
@@ -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()
}
}
@@ -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),
@@ -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 {
+37 -69
View File
@@ -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)
+1 -2
View File
@@ -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"
+2 -1
View File
@@ -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"
-1
View File
@@ -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>
-2
View File
@@ -16,8 +16,6 @@
<resources>
<string name="app_name">EhViewer</string>
<!-- Category -->
<string name="homepage">Página de inicio</string>
-2
View File
@@ -16,8 +16,6 @@
<!-- French translations by UShimamura -->
<resources>
<string name="app_name">EhViewer</string>
<!-- Category -->
<string name="homepage">Page d\'acceuil</string>
-2
View File
@@ -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>
-2
View File
@@ -16,8 +16,6 @@
<resources>
<string name="app_name">EhViewer</string>
<!-- Theme -->
<string name="homepage">홈</string>
+261
View File
@@ -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>
-2
View File
@@ -16,8 +16,6 @@
<resources>
<string name="app_name">EhViewer</string>
<!-- Theme -->
<string name="homepage">หน้าหลัก</string>
+17 -77
View File
@@ -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">设置-&gt;Eh-&gt;画廊站点-&gt;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">请至 设置-&gt;EH-&gt;我的标签 订阅标签</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">保存到&#8230;</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>
+59 -38
View File
@@ -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">儲存到&#8230;</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>
+4 -1
View File
@@ -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">
+30 -91
View File
@@ -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-&gt;Eh-&gt;Gallery Site-&gt;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-&gt;EH-&gt;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&#8230;)</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&#8230;</string>
<string name="action_save_to">Save to…</string>
<string name="wide_color_gamut">Use Display P3 color space</string>
<!-- Tachiyomi Reader End-->
</resources>
+11 -4
View File
@@ -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"
-11
View File
@@ -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"
+1
View File
@@ -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
View File
@@ -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
}