Compare commits

...
40 Commits
Author SHA1 Message Date
FooIbar 571969fca9 Prepare for release 1.13.1
Releases / build (push) Has been cancelled
2024-11-11 21:14:29 +08:00
FooIbar 16fa52790c Misc cleanups (#1921)
* Clean up resources

* Clean up code
2024-11-11 20:39:45 +08:00
FooIbar c88c252587 Update README (#1920)
[no ci]
2024-11-11 17:19:43 +08:00
FooIbar 253cb03de8 Refine AndroidFileSystem exception handling 2024-11-11 17:19:17 +08:00
renovate[bot] 0b74adc13b Lock file maintenance (#1917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 01:17:16 +00:00
FooIbar 6fc2328475 Fix false positive download failures on API 28 2024-11-11 00:52:12 +08:00
renovate[bot] e9c4201acc Update dependency io.coil-kt.coil3:coil-bom to v3.0.2 (#1912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 04:10:41 +00:00
FooIbar 9edb52c909 Remove desktop site option 2024-11-10 00:21:53 +08:00
FooIbar f4c7320bb5 Remove search cover feature due to upstream change 2024-11-09 22:54:07 +08:00
FooIbar 17581ca204 Use desktop User-Agent by default
Since WebP resamples have smaller file size.
2024-11-09 20:56:14 +08:00
FooIbar 11edb8ce5d Error out on cover search for V2 galleries 2024-11-09 20:35:09 +08:00
FooIbar 40caa7b1eb Prefer to get page tokens from MPV 2024-11-09 19:44:12 +08:00
FooIbar e3f4c24983 Extend update download request timeout 2024-11-09 13:59:35 +08:00
renovate[bot]andDude so hot dd0482cb26 Allow ExchangeSite on Http 500 (#1905)
* Allow ExchangeSite on Http 500

* Fixup

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
2024-11-09 13:38:23 +08:00
FooIbar 4052007de8 Implement retry on click for thumbnails 2024-11-09 11:27:10 +08:00
FooIbar 85b03d9d8a Propagate errors from writeComicInfo to callers 2024-11-08 16:06:55 +08:00
renovate[bot] 36c3fd6f5d Update dependency com.google.devtools.ksp to v2.1.0-RC-1.0.27 (#1902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 07:15:42 +00:00
renovate[bot] d397a5d35e Update dependency com.android.tools:desugar_jdk_libs to v2.1.3 2024-11-07 09:59:04 +00:00
renovate[bot] 43d83ad919 Update dependency io.coil-kt.coil3:coil-bom to v3.0.1 2024-11-07 09:58:36 +00:00
FooIbar e71dd1d576 Improve scrollbar a11y 2024-11-06 23:44:48 +08:00
renovate[bot] 72e28b8e04 Update dependency com.google.devtools.ksp to v2.1.0-RC-1.0.26 2024-11-05 20:05:19 +00:00
renovate[bot] 3432a15c62 Update dependency io.github.pdvrieze.xmlutil:serialization to v0.90.3 2024-11-05 09:46:27 +00:00
renovate[bot]andDude so hot 45a5bae8b2 Group rust ffi code (#1893)
* Group rust ffi code

Signed-off-by: Dude so hot <djohn@fbi.gov>

* Fix

Signed-off-by: Dude so hot <djohn@fbi.gov>

* Fix

Signed-off-by: Dude so hot <djohn@fbi.gov>

* Refine

Signed-off-by: Dude so hot <djohn@fbi.gov>

---------

Signed-off-by: Dude so hot <djohn@fbi.gov>
Co-authored-by: Dude so hot <djohn@fbi.gov>
2024-11-05 09:39:03 +00:00
a566393a89 Optimize to use decode buffer pool (#1890)
* Optimize to use decode buffer poll

* Drop unnecessary logging

* Refine

* Update app/src/main/cpp/archive.c

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-11-05 05:01:15 +00:00
renovate[bot] 5f2f3f9122 Update dependency io.coil-kt.coil3:coil-bom to v3.0.0 2024-11-04 22:53:10 +00:00
renovate[bot] c7fccf7e70 Update Kotlin & KSP to v2.1.0-RC 2024-11-04 20:24:35 +00:00
FooIbar 3d92d061f1 Fix archive size check 2024-11-04 21:15:07 +08:00
Dude so hot f2759675a1 Optimize to not allocate anon mapping when fully zero copy 2024-11-04 21:12:45 +08:00
Dude so hot 2e6ff78662 Fast fix for operator precedence 2024-11-04 20:57:10 +08:00
renovate[bot]andDude so hot 8ca0a32932 Cleanup to not handle memory allocation failure (#1882)
* Cleanup to not handle memory allocation failure

* Handle open failed on first open

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
2024-11-04 12:06:38 +00:00
FooIbar 72fbcd4e0e Refine Cronet workaround 2024-11-04 18:43:16 +08:00
a5719a9c1d Optimize zero copy case to not iter over entries (#1879)
* Optimie zero copy case to not iter over entries

* Update app/src/main/cpp/archive.c

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-11-04 09:03:41 +00:00
Re*Index. (ot_inc) bad41d5027 Change funds text (#1878)
* Fix Japanese

* Change Funds text
2024-11-04 08:49:11 +00:00
renovate[bot] 3bfb95f190 Lock file maintenance 2024-11-04 00:09:54 +00:00
renovate[bot]andDude so hot f04695feb6 Refresh Avatar Url when load failed (#1873)
* Refresh Avatar Url when load failed

* Check 404

* Refine

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
2024-11-03 15:17:50 +00:00
renovate[bot]andDude so hot 94da911f93 Fix DrawerContent ColorScheme
* Fix listItemOnDrawerColor()

* Fix topBarOnDrawerColor

* Refine location

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
2024-11-02 04:51:06 +00:00
Re*Index. (ot_inc) f4771d4ddb Fix Japanese 2024-11-02 11:06:44 +08:00
Re*Index. (ot_inc) 9f7c4adb0f Make Japanese README & Fix Japanese (#1867)
* Update Japanese

* Add Japanese README

* Update ja-jp.md

* Update ja-jp.md

* Update ja-jp.md

* Update ja-jp.md

* Update ja-jp.md

* Update ja-jp.md

* fix text & suffix.

* Update ja.md

* Update ja.md

* Update ja.md

* Update ja.md

* Add Minimum Android Version text
2024-11-01 23:41:58 +08:00
FooIbar 6fb20fc6c1 [no ci] Update README 2024-11-01 21:55:55 +08:00
FooIbar ce8e91aab8 Prepare for next iteration 2024-11-01 21:03:47 +08:00
80 changed files with 1016 additions and 883 deletions

No files matched your search

+19
View File
@@ -1,5 +1,24 @@
# 变更日志
## [1.13.1] - 2024-11-11
### 重大变更
* 由于网站限制,移除搜索封面功能
* 移除桌面版网站选项,默认使用 1280x 分辨率
### 改进
* 更新日本语翻译
* 优化压缩包读取
* 缓解某些小米系统上应用闪退 [#1826](https://github.com/FooIbar/EhViewer/issues/1826)
* 杂项优化
### Bug 修复
* Android 9 上的下载失败误报 [#1914](https://github.com/FooIbar/EhViewer/issues/1914)
## [1.13.0] - 2024-11-01
### 重大变更
+11 -5
View File
@@ -8,6 +8,10 @@
<a href="/docs/README/zh-tw.md">
正體中文
</a>
<span> | </span>
<a href="/docs/README/ja.md">
日本語
</a>
</p>
<h1 align="center">
@@ -59,17 +63,19 @@
# Description
EhViewer fork dedicated to lightweight and high-performance
A modern EhViewer fork dedicated to high-performance
with [Material Design 3](https://m3.material.io/)
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# Download
| Flavor | Feature |
|-------------|----------------------------------|
| Default | Android 8.0+, full support |
| Marshmallow | Android 6.0-7.1, limited support |
| Flavor | Minimum Android Version | Notes |
|-------------|-------------------------|-------------------------------------------|
| Default | 8.0 | Animated WebP support requires Android 9+ |
| Marshmallow | 6.0* | Limited support, no guarantees |
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
+2 -2
View File
@@ -78,8 +78,8 @@ android {
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
versionCode = 180061
versionName = "1.13.0"
versionCode = 180062
versionName = "1.13.1"
// versionNameSuffix = "-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
+3 -1
View File
@@ -111,7 +111,9 @@ FetchContent_MakeAvailable(Corrosion)
corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml LOCKED)
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
if (ANDROID_NATIVE_API_LEVEL GREATER_EQUAL 26)
corrosion_set_features(ehviewer_rust FEATURES api-level-26)
corrosion_set_features(ehviewer_rust FEATURES android-26)
else ()
corrosion_set_features(ehviewer_rust FEATURES android)
endif ()
# Build and link our app's native lib
+124 -163
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Tarsin Norbin
* Copyright 2022-2024 Tarsin Norbin
*
* This file is part of EhViewer
*
@@ -45,31 +45,25 @@ typedef struct {
typedef struct {
const char *filename;
int index;
size_t size;
ssize_t size;
void *addr;
} entry;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static archive_ctx **ctx_pool = NULL;
#define CTX_POOL_SIZE 20
#define MAX_PARALLEL_DECOMP 4
#define max(a, b) ((a) > (b) ? (a) : (b))
static void *mempool = MAP_FAILED;
static size_t *mempoolofs = NULL;
static int page_size = 0;
#define PAGE_ALIGN(x) ((x + page_size - 1) & ~(page_size - 1))
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
#define PROT_RW (PROT_WRITE | PROT_READ)
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE)
static pthread_mutex_t ctx_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static archive_ctx **ctx_pool = NULL;
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
static void *decode_buffer[MAX_PARALLEL_DECOMP];
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = MAP_FAILED;
static size_t archiveSize = 0;
static entry *entries = NULL;
static size_t entryCount = 0;
static ssize_t max_file_size = 0;
const char supportExt[10][6] = {
"jpeg",
@@ -117,52 +111,64 @@ static inline int compare_entries(const void *a, const void *b) {
return strnatcmp(fa, fb);
}
static int archive_map_entries_index(archive_ctx *ctx, bool sort) {
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
static bool fill_entry_zero_copy(struct archive *arc, entry *entry) {
void *buffer = NULL;
size_t buffer_size = 0;
la_int64_t output_ofs = 0;
archive_read_data_block(arc, (const void **) &buffer, &buffer_size, &output_ofs);
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == entry->size;
entry->addr = zero_copy ? buffer : NULL;
return zero_copy;
}
static void archive_map_entries_index(archive_ctx *ctx, bool sort) {
int count = 0;
bool zero_copy = true;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
const char *name = archive_entry_pathname2(ctx->entry);
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
entries[count].size = archive_entry_size(ctx->entry);
ssize_t size = archive_entry_size(ctx->entry);
max_file_size = max(size, max_file_size);
entries[count].size = size;
// We don't expect zero copy if first content can't do zero copy
if (zero_copy) zero_copy = fill_entry_zero_copy(ctx->arc, &entries[count]);
count++;
}
}
if (sort)
qsort(entries, entryCount, sizeof(entry), compare_entries);
return count;
if (sort) qsort(entries, entryCount, sizeof(entry), compare_entries);
}
static bool archive_prealloc_mempool() {
mempoolofs = calloc(entryCount, sizeof(size_t));
for (int i = 0; i < entryCount; ++i) {
if (!i)
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
else
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
static void *acquire_decode_buffer() {
void *addr = NULL;
pthread_mutex_lock(&buffer_mutex);
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
addr = decode_buffer[i];
if (addr) {
decode_buffer[i] = NULL;
break;
}
}
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
if (mempool == MAP_FAILED) {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return false;
}
return true;
pthread_mutex_unlock(&buffer_mutex);
if (!addr) addr = malloc(max_file_size);
return addr;
}
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
static void mempool_release_pages(void *addr, size_t size) {
size = PAGE_ALIGN(size);
madvise_log_if_error(addr, size, MADV_DONTNEED);
}
static bool kernel_can_prefault = true;
static void mempool_prefault_pages(void *addr, size_t size) {
size = PAGE_ALIGN(size);
if (kernel_can_prefault) {
if (madvise(addr, size, MADV_POPULATE_WRITE)) kernel_can_prefault = false;
static void release_decode_buffer(void *buffer) {
pthread_mutex_lock(&buffer_mutex);
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
void *addr = decode_buffer[i];
if (!addr) {
decode_buffer[i] = buffer;
pthread_mutex_unlock(&buffer_mutex);
return;
}
}
pthread_mutex_unlock(&buffer_mutex);
free(buffer);
}
static int archive_list_all_entries(archive_ctx *ctx) {
@@ -181,18 +187,10 @@ static void archive_release_ctx(archive_ctx *ctx) {
}
}
static int archive_alloc_ctx(archive_ctx **ctxptr) {
archive_ctx *ctx;
int err;
ctx = calloc(1, sizeof(archive_ctx));
if (!ctx)
return -ENOMEM;
static archive_ctx *archive_alloc_ctx() {
archive_ctx *ctx = calloc(1, sizeof(archive_ctx));
ctx->arc = archive_read_new();
ctx->using = 1;
if (!ctx->arc) {
free(ctx);
return -ENOMEM;
}
archive_read_support_format_tar(ctx->arc);
archive_read_support_format_7zip(ctx->arc);
archive_read_support_format_rar5(ctx->arc);
@@ -202,14 +200,14 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(ctx->arc, passwd);
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err) {
int err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err < ARCHIVE_OK) {
LOGE("%s%s", "Open archive failed: ", archive_error_string(ctx->arc));
archive_read_free(ctx->arc);
free(ctx);
return err;
return NULL;
}
*ctxptr = ctx;
return 0;
return ctx;
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
@@ -226,7 +224,7 @@ static int archive_skip_to_index(archive_ctx *ctx, int index) {
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&ctx_pool_mutex);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
@@ -241,16 +239,14 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&ctx_pool_mutex);
if (!ctx) {
archive_ctx *victimCtx = NULL;
int victimIdx = 0;
int replace = 1;
ret = archive_alloc_ctx(&ctx);
if (ret)
return ret;
pthread_mutex_lock(&mutex);
ctx = archive_alloc_ctx();
pthread_mutex_lock(&ctx_pool_mutex);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
@@ -265,7 +261,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
}
if (replace) ctx_pool[victimIdx] = ctx;
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&ctx_pool_mutex);
if (replace) archive_release_ctx(victimCtx);
}
ret = archive_skip_to_index(ctx, idx);
@@ -283,7 +279,6 @@ JNIEXPORT jint JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
EH_UNUSED(env);
EH_UNUSED(thiz);
page_size = getpagesize();
archive_ctx *ctx = NULL;
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (archiveAddr == MAP_FAILED) {
@@ -292,106 +287,72 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
}
archiveSize = size;
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
LOGE("Allocate archive ctx pool failed:ENOMEM");
ctx = archive_alloc_ctx();
if (!ctx) return 0;
entryCount = archive_list_all_entries(ctx);
LOGI("%s%zu%s", "Found ", entryCount, " images in archive");
if (!entryCount) {
archive_release_ctx(ctx);
return 0;
}
int r = archive_alloc_ctx(&ctx);
if (r) {
if (r == -ENOMEM)
LOGE("%s", "Mem alloc failed");
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
return 0;
} else {
entryCount = archive_list_all_entries(ctx);
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
if (!entryCount) {
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return 0;
}
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
break;
case 0: // format supports but no encrypted entry found
default:
need_encrypt = false;
}
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR_V5:
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
break;
case 0: // format supports but no encrypted entry found
default:
need_encrypt = false;
}
archive_release_ctx(ctx);
r = archive_alloc_ctx(&ctx);
if (r) {
if (r == -ENOMEM)
LOGE("%s", "Mem alloc failed");
r = 0;
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
entries = calloc(entryCount, sizeof(entry));
r = archive_map_entries_index(ctx, sort_entries);
if (!archive_prealloc_mempool()) {
r = 0;
}
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR_V5:
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
archive_release_ctx(ctx);
return r;
ctx = archive_alloc_ctx();
entries = calloc(entryCount, sizeof(entry));
archive_map_entries_index(ctx, sort_entries);
archive_release_ctx(ctx);
return (int) entryCount;
}
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = entries[index].size;
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret = archive_get_ctx(&ctx, index);
if (ret) return 0;
void *buffer = NULL;
size_t buffer_size = 0;
la_int64_t output_ofs = 0;
archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == size;
if (zero_copy) {
ctx->using = 0;
return (*env)->NewDirectByteBuffer(env, buffer, buffer_size);
entry *entry = &entries[index];
ssize_t size = entry->size;
if (entry->addr) {
return (*env)->NewDirectByteBuffer(env, entry->addr, size);
} else {
size_t bytes = 0;
mempool_prefault_pages(addr, size);
do {
memcpy(addr + output_ofs, buffer, buffer_size);
bytes += buffer_size;
ret = archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
} while (bytes != size && !ret);
ctx->using = 0;
if (bytes == size) {
return (*env)->NewDirectByteBuffer(env, addr, size);
} else {
if (ret < 0) {
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
archive_ctx *ctx = NULL;
if (!archive_get_ctx(&ctx, entry->index)) {
void *addr = acquire_decode_buffer();
ssize_t bytes = archive_read_data(ctx->arc, addr, size);
ctx->using = 0;
if (bytes == size) {
return (*env)->NewDirectByteBuffer(env, addr, size);
} else {
LOGE("%s", "No enough data read, WTF?");
if (bytes < 0) {
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
} else {
LOGE("%s", "No enough data read, WTF?");
}
}
release_decode_buffer(addr);
}
mempool_release_pages(addr, size);
}
return 0;
}
@@ -413,12 +374,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
munmap(archiveAddr, archiveSize);
archiveAddr = MAP_FAILED;
}
if (mempool != MAP_FAILED) {
munmap(mempool, MEMPOOL_SIZE);
mempool = MAP_FAILED;
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
free(decode_buffer[i]);
decode_buffer[i] = NULL;
}
free(mempoolofs);
mempoolofs = NULL;
max_file_size = 0;
if (entries) {
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
@@ -445,8 +405,8 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
passwd = realloc(passwd, len + 1);
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
passwd[len] = 0;
archive_alloc_ctx(&ctx);
void *tmpBuf = alloca(4096);
ctx = archive_alloc_ctx();
char tmpBuf[4096];
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
if (!archive_entry_is_playable(entry))
continue;
@@ -489,8 +449,9 @@ JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (!ADDR_IN_FILE_MAPPING(addr)) mempool_release_pages(addr, size);
if (!ADDR_IN_FILE_MAPPING(addr)) {
release_decode_buffer(addr);
}
}
JNIEXPORT void JNICALL
@@ -515,7 +476,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz,
archive_entry_copy_stat(entry, &st);
archive_entry_set_perm(entry, 0644);
archive_write_header(arc, entry);
int len;
size_t len;
do {
len = read(fd, buff, sizeof(buff));
archive_write_data(arc, buff, len);
@@ -185,7 +185,7 @@ private class AnchoredHorizontalDraggableNode<T>(
}
}
override fun onDragStarted(startedPosition: Offset) { }
override fun onDragStarted(startedPosition: Offset) = Unit
override fun onDragStopped(velocity: Velocity) {
if (!isAttached) return
@@ -28,6 +28,7 @@ import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -78,6 +79,7 @@ val LocalScrollbarStyle = staticCompositionLocalOf { defaultScrollbarStyle() }
data class ScrollbarStyle(
val minimalHeight: Dp,
val thickness: Dp,
val padding: Dp,
val shape: Shape,
val hoverDurationMillis: Int,
val unhoverColor: Color,
@@ -90,6 +92,7 @@ data class ScrollbarStyle(
fun defaultScrollbarStyle() = ScrollbarStyle(
minimalHeight = 16.dp,
thickness = 8.dp,
padding = 0.dp,
shape = RoundedCornerShape(4.dp),
hoverDurationMillis = 300,
unhoverColor = Color.Black.copy(alpha = 0.12f),
@@ -239,7 +242,7 @@ private fun Scrollbar(
)
}
val scrollThickness = style.thickness.roundToPx()
val scrollThickness = (style.thickness + style.padding * 2).roundToPx()
val measurePolicy = if (isVertical) {
remember(sliderAdapter, scrollThickness) {
verticalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
@@ -261,7 +264,11 @@ private fun Scrollbar(
Layout(
{
Box(
Modifier
if (isVertical) {
Modifier.padding(horizontal = style.padding)
} else {
Modifier.padding(vertical = style.padding)
}
.background(if (isVisible) color else Color.Transparent, style.shape)
.scrollbarDrag(
interactionSource = interactionSource,
@@ -30,6 +30,7 @@ import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import coil3.network.ConnectivityChecker
import coil3.network.NetworkFetcher
import coil3.network.ktor3.asNetworkClient
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.crossfade
@@ -43,6 +44,7 @@ import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.coil.exchangeSite
import com.hippo.ehviewer.coil.limitConcurrency
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.SearchDatabase
@@ -161,7 +163,12 @@ class EhApplication :
interceptorCoroutineContext(Dispatchers.Default)
components {
serviceLoaderEnabled(false)
add(NetworkFetcher.Factory({ ktorClient.limitConcurrency() }) { ConnectivityChecker.ONLINE })
add(
NetworkFetcher.Factory(
networkClient = { ktorClient.asNetworkClient().limitConcurrency().exchangeSite() },
connectivityChecker = { ConnectivityChecker.ONLINE },
),
)
add(MergeInterceptor)
add(DownloadThumbInterceptor)
if (isAtLeastO) {
@@ -196,7 +203,7 @@ class EhApplication :
companion object {
val ktorClient by lazy {
if (isAtLeastSExtension7 && Settings.enableCronet) {
if (isAtLeastSExtension7 && Settings.enableCronet.value) {
HttpClient(Cronet) {
engine { configureClient() }
configureCommon()
@@ -126,6 +126,7 @@ object Settings : DataStorePreferences(null) {
val qSSaveProgress = boolPref("qs_save_progress", true)
val security = boolPref("require_unlock", false)
val animateItems = boolPref("animate_items", true)
val enableCronet = boolPref("enable_cronet", true)
val displayName = stringOrNullPref("display_name", null)
val avatar = stringOrNullPref("avatar", null)
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
@@ -164,8 +165,7 @@ object Settings : DataStorePreferences(null) {
var downloadOriginImage by boolPref("download_origin_image", false)
var saveAsCbz by boolPref("save_as_cbz", false)
var archiveMetadata by boolPref("archive_metadata", true)
var enableCronet by boolPref("enable_cronet", true)
var desktopSite by boolPref("desktop_site", false)
var desktopSite by boolPref("desktop_site", true)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
var securityDelay by intPref("require_unlock_delay", 0)
@@ -210,9 +210,6 @@ object Settings : DataStorePreferences(null) {
val showNavigationOverlayOnStart = boolPref("reader_navigation_overlay_on_start", false)
val stripExtraneousAds = boolPref("strip_extraneous_ads", false)
val userAgent
get() = if (desktopSite) CHROME_USER_AGENT else CHROME_MOBILE_USER_AGENT
init {
if ("CN" == Locale.getDefault().country) {
edit {
@@ -254,6 +251,3 @@ object Settings : DataStorePreferences(null) {
}
}
}
private const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
private const val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
@@ -44,16 +44,15 @@ val String.isV2PreviewKey
get() = startsWith('$')
val GalleryInfo.thumbUrl
get() = thumbKey!!.let {
if (it.endsWith("webp")) {
if (useExThumb) URL_PREFIX_THUMB_EX else URL_PREFIX_THUMB_E
} else {
v1ThumbPrefix
} + it
}
get() = keyToUrl(thumbKey!!)
val v1ThumbPrefix
get() = if (useExThumb) URL_PREFIX_V1_THUMB_EX else URL_PREFIX_THUMB_E
fun keyToUrl(key: String, exSite: Boolean = useExThumb) = if (key.endsWith("webp")) {
if (exSite) URL_PREFIX_THUMB_EX else URL_PREFIX_THUMB_E
} else {
if (exSite) URL_PREFIX_V1_THUMB_EX else URL_PREFIX_THUMB_E
} + key
fun flipThumbSite(url: String) = keyToUrl(getThumbKey(url), !useExThumb)
private val useExThumb
get() = EhUtils.isExHentai && !Settings.forceEhThumb.value
@@ -13,11 +13,12 @@ object EhCookieStore : CookiesStorage {
fun hasSignedIn(): Boolean = getCookies(EhUrl.HOST_E)?.run {
containsKey(KEY_IPB_MEMBER_ID) && containsKey(KEY_IPB_PASS_HASH)
} ?: false
} == true
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private const val KEY_HATH_PERKS = "hath_perks"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private const val KEY_UTMP_NAME = "__utmp"
@@ -35,6 +36,8 @@ object EhCookieStore : CookiesStorage {
fun getUserId() = getCookies(EhUrl.HOST_E)?.get(KEY_IPB_MEMBER_ID)
fun getHathPerks() = getCookies(EhUrl.HOST_E)?.get(KEY_HATH_PERKS)?.substringBefore('-')
fun getIdentityCookies(): List<Pair<String, String?>> {
val eCookies = getCookies(EhUrl.HOST_E)
val exCookies = getCookies(EhUrl.HOST_EX)
@@ -55,8 +55,8 @@ object EhFilter : CoroutineScope {
suspend fun needTags() = filters.await().any { it.enable && (it.mode == FilterMode.TAG || it.mode == FilterMode.TAG_NAMESPACE) }
suspend fun filterTitle(info: GalleryInfo) = anyActive(FilterMode.TITLE) { info.title.orEmpty().contains(it.text, true) }
suspend fun filterUploader(info: GalleryInfo) = anyActive(FilterMode.UPLOADER) { it.text == info.uploader }
suspend fun filterTag(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG) { matchTag(tag, it.text.lowercase()) } } ?: false
suspend fun filterTagNamespace(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG_NAMESPACE) { matchTagNamespace(tag, it.text.lowercase()) } } ?: false
suspend fun filterTag(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG) { matchTag(tag, it.text.lowercase()) } } == true
suspend fun filterTagNamespace(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG_NAMESPACE) { matchTagNamespace(tag, it.text.lowercase()) } } == true
suspend fun filterCommenter(commenter: String) = anyActive(FilterMode.COMMENTER) { it.text == commenter }
suspend fun filterComment(comment: String) = anyActive(FilterMode.COMMENT) { regex(it).containsMatchIn(comment) }
}
@@ -15,18 +15,32 @@
*/
package com.hippo.ehviewer.client
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Environment
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import arrow.core.memoize
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.parser.Archive
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.addTextToClipboard
import com.materialkolor.hct.Hct
import com.materialkolor.ktx.from
import com.materialkolor.ktx.toColor
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlin.math.abs
import splitties.systemservices.downloadManager
object EhUtils {
const val NONE = -1 // Use it for homepage
@@ -87,6 +101,9 @@ object EhUtils {
val isExHentai: Boolean
get() = Settings.gallerySite.value == EhUrl.SITE_EX
val isMpvAvailable
get() = EhCookieStore.getHathPerks()?.contains('q') == true
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first) {
@@ -175,4 +192,35 @@ object EhUtils {
// TODO But not sure every '|' means that
return title.substringBeforeLast('|').trim().ifEmpty { null }
}
context(Context)
suspend fun downloadArchive(galleryDetail: GalleryDetail, archive: Archive) {
val gid = galleryDetail.gid
EhEngine.downloadArchive(gid, galleryDetail.token, archive.res, archive.isHAtH)?.let {
val uri = it.toUri()
val intent = Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(uri, "application/zip")
}
val name = "$gid-${getSuitableTitle(galleryDetail)}.zip"
try {
startActivity(intent)
withUIContext { addTextToClipboard(name, true) }
} catch (_: ActivityNotFoundException) {
val r = DownloadManager.Request(uri)
r.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
)
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
downloadManager.enqueue(r)
}
if (Settings.archiveMetadata) {
SpiderDen(galleryDetail).apply {
initDownloadDir()
writeComicInfo()
}
}
}
}
}
@@ -16,7 +16,7 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.v1ThumbPrefix
import com.hippo.ehviewer.client.keyToUrl
import kotlinx.parcelize.Parcelize
sealed interface GalleryPreview : Parcelable {
@@ -30,7 +30,7 @@ data class V1GalleryPreview(
override val imageKey: String,
override val position: Int,
) : GalleryPreview {
override val url get() = v1ThumbPrefix + imageKey
override val url get() = keyToUrl(imageKey)
}
@Parcelize
@@ -400,8 +400,6 @@ object GalleryDetailParser {
hasMore = true
}
}
override fun tail(node: Node, depth: Int) {}
},
chd!!,
)
@@ -41,7 +41,7 @@ object ParserUtils {
fun parseDate(str: String): Long = formatter.parse(str).toEpochMillis()
fun trim(str: String?): String = str?.unescapeXml()?.trim() ?: ""
fun trim(str: String?): String = str?.unescapeXml()?.trim().orEmpty()
fun parseInt(str: String?, defValue: Int): Int = trim(str).replace(",", "").toIntOrDefault(defValue)
@@ -0,0 +1,27 @@
package com.hippo.ehviewer.coil
import coil3.network.HttpException
import coil3.network.NetworkClient
import coil3.network.NetworkRequest
import coil3.network.NetworkResponse
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.flipThumbSite
class ExchangeSiteNetworkClient(val impl: NetworkClient) : NetworkClient {
override suspend fun <T> executeRequest(req: NetworkRequest, f: suspend (NetworkResponse) -> T): T {
if (EhUtils.isExHentai) {
try {
return impl.executeRequest(req, f)
} catch (e: HttpException) {
if (e.response.code == 500) {
return impl.executeRequest(req.copy(url = flipThumbSite(req.url)), f)
}
throw e
}
} else {
return impl.executeRequest(req, f)
}
}
}
fun NetworkClient.exchangeSite() = ExchangeSiteNetworkClient(this)
@@ -3,10 +3,8 @@ package com.hippo.ehviewer.coil
import coil3.network.NetworkClient
import coil3.network.NetworkRequest
import coil3.network.NetworkResponse
import coil3.network.ktor3.asNetworkClient
import com.hippo.ehviewer.client.URL_PREFIX_THUMB_EX
import com.hippo.ehviewer.client.URL_SIGNATURE_THUMB_NORMAL
import io.ktor.client.HttpClient
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import moe.tarsin.coroutines.NamedSemaphore
@@ -27,4 +25,4 @@ class LimitConcurrencyNetworkClient(val impl: NetworkClient) : NetworkClient {
}
}
fun HttpClient.limitConcurrency() = LimitConcurrencyNetworkClient(asNetworkClient())
fun NetworkClient.limitConcurrency() = LimitConcurrencyNetworkClient(this)
@@ -56,7 +56,7 @@ suspend fun <T> useArchivePageLoader(
{ openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip")) },
{ _, _ -> closeArchive() },
)
check(size >= 0) { "Archive have no content!" }
check(size > 0) { "Archive have no content!" }
if (needPassword() && archivePasswds.filterNotNull().none { providePassword(it) }) {
archivePasswds += passwdProvider { providePassword(it) }
}
@@ -1,6 +1,6 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.Settings.userAgent
import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.client.EhCookieStore
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
@@ -12,8 +12,9 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
private const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
private const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
private const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
private const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: Boolean = true) = apply {
install(HttpCookies) {
@@ -23,7 +24,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
requestTimeoutMillis = 10_000
}
install(UserAgent) {
agent = userAgent
agent = CHROME_USER_AGENT
}
defaultRequest {
header(HttpHeaders.Accept, CHROME_ACCEPT)
@@ -4,6 +4,7 @@ package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine
import android.net.http.HttpException
import android.net.http.NetworkException
import android.net.http.UploadDataProvider
import android.net.http.UploadDataSink
import android.net.http.UrlRequest
@@ -11,6 +12,7 @@ import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.restartApplication
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.callContext
import io.ktor.client.plugins.HttpTimeoutCapability
@@ -39,6 +41,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.preferences.edit
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking
@@ -103,8 +106,11 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
// Cronet may crash on some devices, fuck xiaomi
// https://github.com/FooIbar/EhViewer/issues/1826
if (error.message?.contains("ERR_FILE_NOT_FOUND") == true) {
Settings.enableCronet = false
if (error is NetworkException && error.errorCode == NetworkException.ERROR_OTHER) {
Settings.edit(true) {
enableCronet.value = false
}
config.context.restartApplication()
}
if (continuation.isActive) {
continuation.resumeWithException(error)
@@ -284,7 +284,7 @@ class SpiderDen(val info: GalleryInfo) {
FileUtils.getExtensionFromFilename(source.name)!!
}
override fun close() {}
override fun close() = Unit
}
}
@@ -352,19 +352,15 @@ class SpiderDen(val info: GalleryInfo) {
suspend fun writeComicInfo(fetchMetadata: Boolean = true) {
downloadDir?.run {
resolve(COMIC_INFO_FILE).also {
runCatching {
if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info))
if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info))
}
info.getComicInfo().apply {
write(it)
DownloadManager.getDownloadInfo(gid)?.let { downloadInfo ->
downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty())
EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList)
}
info.getComicInfo().apply {
write(it)
DownloadManager.getDownloadInfo(gid)?.let { downloadInfo ->
downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty())
EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList)
}
}
}.onFailure {
logcat(it)
}
}
}
@@ -25,6 +25,7 @@ import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.ehRequest
import com.hippo.ehviewer.client.exception.QuotaExceededException
@@ -224,7 +225,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
if (!spiderDen.postArchive()) {
if (mWorkerScope.isDownloadMode) {
spiderDen.writeComicInfo()
runCatching {
spiderDen.writeComicInfo()
}.onFailure {
logcat(it)
}
}
runCatching {
writeSpiderInfoToLocal()
@@ -323,8 +328,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
spiderInfo
}
private val isMpvAvailable = EhUtils.isMpvAvailable
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
val spiderInfo = spiderInfo
if (!isMpvAvailable) return null
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid,
galleryInfo.token,
@@ -343,8 +350,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
suspend fun getPTokenFromInternet(index: Int): String? {
val spiderInfo = spiderInfo
// Check previewIndex
var previewIndex: Int
previewIndex = if (spiderInfo.previewPerPage >= 0) {
@@ -534,25 +539,19 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private suspend fun doInJob(index: Int, force: Boolean, orgImg: Boolean, skipHath: Boolean) {
suspend fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: getPTokenFromInternet(index)
?: getPTokenFromInternet(index)
return spiderInfo.pTokenMap[index]
?: getPTokenFromMultiPageViewer(index)
?: getPTokenFromInternet(index)
// Preview size may changed, so try to get pToken twice
?: getPTokenFromInternet(index)
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
if (force) {
if (spiderInfo.pTokenMap[index] == TOKEN_FAILED) {
spiderInfo.pTokenMap.remove(index)
}
} else if (index in spiderDen) {
if (!force && index in spiderDen) {
return updatePageState(index, STATE_FINISHED)
}
pToken = getPToken(index)
?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE).also {
spiderInfo.pTokenMap[index] = TOKEN_FAILED
}
pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE)
previousPToken = getPToken(index - 1)
// The lock for delay should be acquired before anything else to maintain FIFO order
@@ -618,7 +617,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
originImageUrl += "?nl=$skipHathKey"
}
val pageUrl = EhUrl.getPageUrl(spiderInfo.gid, index, pToken)
EhEngine.getOriginalImageUrl(originImageUrl!!, pageUrl) to EhUrl.referer
EhEngine.getOriginalImageUrl(originImageUrl!!, pageUrl) to referer
} else {
// Original image url won't change, so only set forceHtml in this case
forceHtml = true
@@ -40,12 +40,14 @@ import arrow.core.Option
import arrow.core.none
import arrow.core.some
import coil3.compose.AsyncImage
import coil3.network.HttpException
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.login.refreshAccountInfo
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.displayString
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -179,6 +181,12 @@ fun AvatarIcon() {
AsyncImage(
model = avatar,
contentDescription = null,
onError = { (_, r) ->
val e = r.throwable
if (e is HttpException && e.response.code == 404) {
launchIO { refreshAccountInfo() }
}
},
modifier = Modifier.clip(CircleShape),
contentScale = ContentScale.Crop,
)
@@ -1,6 +1,9 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Card
import androidx.compose.material3.ShapeDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
@@ -12,8 +15,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.Image
import coil3.Image as CoilImage
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import coil3.compose.ConstraintsSizeResolver
import coil3.compose.rememberAsyncImagePainter
import coil3.compose.rememberConstraintsSizeResolver
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ktbuilder.imageRequest
@@ -25,7 +32,25 @@ import com.hippo.ehviewer.ui.tools.shouldCrop
@NonRestartableComposable
fun requestOf(model: GalleryInfo): ImageRequest {
val context = LocalContext.current
return remember(model) { context.imageRequest(model) }
return remember(model) { context.imageRequest(model) }.withSizeResolver()
}
@Composable
@NonRestartableComposable
fun ImageRequest.withSizeResolver() = if (defined.sizeResolver != null) {
this
} else {
val sizeResolver = rememberConstraintsSizeResolver()
remember(this, sizeResolver) { newBuilder().size(sizeResolver).build() }
}
fun Modifier.imageRequest(request: ImageRequest): Modifier {
val sizeResolver = request.sizeResolver
return if (sizeResolver is ConstraintsSizeResolver) {
then(sizeResolver)
} else {
this
}
}
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
@@ -34,7 +59,7 @@ context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
fun EhAsyncThumb(
model: GalleryInfo,
modifier: Modifier = Modifier,
onSuccess: ((Image) -> Unit)? = null,
onSuccess: ((CoilImage) -> Unit)? = null,
contentScale: ContentScale = ContentScale.Fit,
) = AsyncImage(
model = requestOf(model),
@@ -69,3 +94,35 @@ fun EhAsyncCropThumb(
contentScale = contentScale,
)
}
@Composable
fun EhThumbCard(
key: GalleryInfo,
modifier: Modifier = Modifier,
) {
var contentScale by remember(key) { mutableStateOf(ContentScale.Fit) }
val request = requestOf(key)
val painter = rememberAsyncImagePainter(
model = request,
onSuccess = {
if (it.result.image.shouldCrop) {
contentScale = ContentScale.Crop
}
},
)
Card(
onClick = {
if (painter.state.value is AsyncImagePainter.State.Error) {
painter.restart()
}
},
modifier = modifier,
) {
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.imageRequest(request).fillMaxSize().clip(ShapeDefaults.Medium),
contentScale = contentScale,
)
}
}
@@ -109,16 +109,14 @@ fun GalleryDetailHeaderCard(
modifier: Modifier = Modifier,
) = ElevatedCard(modifier = modifier) {
Row {
Card {
with(detailThumbGenerator) {
EhAsyncCropThumb(
key = remember(info.gid) { info },
modifier = Modifier.size(
dimensionResource(id = R.dimen.gallery_detail_thumb_width),
dimensionResource(id = R.dimen.gallery_detail_thumb_height),
),
)
}
with(detailThumbGenerator) {
EhThumbCard(
key = remember(info.gid) { info },
modifier = Modifier.size(
dimensionResource(id = R.dimen.gallery_detail_thumb_width),
dimensionResource(id = R.dimen.gallery_detail_thumb_height),
),
)
}
Spacer(modifier = Modifier.weight(0.5F))
Column(
@@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Badge
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
@@ -108,13 +107,11 @@ fun GalleryInfoListItem(
interactionSource = interactionSource,
) {
Row {
Card {
with(listThumbGenerator) {
EhAsyncCropThumb(
key = info,
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
)
}
with(listThumbGenerator) {
EhThumbCard(
key = info,
modifier = Modifier.aspectRatio(DEFAULT_RATIO),
)
}
ConstraintLayout(
modifier = Modifier.padding(start = 8.dp, top = 2.dp, end = 4.dp, bottom = 4.dp).fillMaxSize(),
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
@@ -20,8 +21,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import coil3.BitmapImage
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter.State
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.V2GalleryPreview
@@ -33,19 +34,19 @@ import com.hippo.ehviewer.ui.tools.shouldCrop
@NonRestartableComposable
fun requestOf(model: GalleryPreview): ImageRequest {
val context = LocalContext.current
return remember(model) { context.imageRequest(model) }
return remember(model) { context.imageRequest(model) }.withSizeResolver()
}
@Composable
fun EhAsyncPreview(
fun EhPreviewCard(
model: GalleryPreview,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
var contentScale by remember(model) { mutableStateOf(ContentScale.Fit) }
AsyncImage(
model = requestOf(model),
contentDescription = null,
modifier = modifier,
val request = requestOf(model)
val painter = rememberAsyncImagePainter(
model = request,
transform = {
if (it is State.Success && model is V2GalleryPreview) {
with(model) {
@@ -74,8 +75,23 @@ fun EhAsyncPreview(
}
}
},
contentScale = contentScale,
)
CrystalCard(
onClick = onClick,
onLongClick = {
if (painter.state.value is State.Error) {
painter.restart()
}
},
modifier = modifier,
) {
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.imageRequest(request).fillMaxSize(),
contentScale = contentScale,
)
}
}
@Composable
@@ -86,16 +102,17 @@ fun EhPreviewItem(
onClick: () -> Unit,
) = Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(contentAlignment = Alignment.Center) {
CrystalCard(
onClick = onClick,
modifier = Modifier.aspectRatio(DEFAULT_RATIO),
) {
if (galleryPreview != null) {
EhAsyncPreview(
model = galleryPreview,
modifier = Modifier.fillMaxSize(),
)
}
if (galleryPreview != null) {
EhPreviewCard(
model = galleryPreview,
onClick = onClick,
modifier = Modifier.aspectRatio(DEFAULT_RATIO),
)
} else {
CrystalCard(
onClick = onClick,
modifier = Modifier.aspectRatio(DEFAULT_RATIO),
) {}
}
}
Text(text = "${position + 1}")
@@ -18,7 +18,7 @@ import androidx.compose.ui.res.stringResource
import com.hippo.ehviewer.R
import kotlinx.coroutines.launch
private val tabs = arrayOf(
private val tabs = intArrayOf(
R.string.pref_category_reading_mode,
R.string.pref_category_general,
R.string.custom_filter,
@@ -19,5 +19,5 @@ fun DownloadsFilterState.take(info: DownloadInfo) = mode.take(info, label) &&
with(info) {
title.containsIgnoreCase(keyword) ||
titleJpn.containsIgnoreCase(keyword) ||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
simpleTags?.any { it.containsIgnoreCase(keyword) } == true
}
@@ -218,6 +218,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
TopAppBar(
title = { Text(text = labelsStr) },
windowInsets = EmptyWindowInsets,
colors = topBarOnDrawerColor(),
actions = {
if (DownloadsFilterMode.CUSTOM == filterMode) {
IconButton(
@@ -314,6 +315,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
headlineContent = {
Text("$allName [$totalCount]")
},
colors = listItemOnDrawerColor(),
)
}
item {
@@ -327,6 +329,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
headlineContent = {
Text("$emptyLabelName [${downloadsCount.getOrDefault(null, 0)}]")
},
colors = listItemOnDrawerColor(),
)
}
@@ -431,6 +434,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
}
},
colors = listItemOnDrawerColor(),
)
}
}
@@ -0,0 +1,17 @@
package com.hippo.ehviewer.ui.screen
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@Composable
fun listItemOnDrawerColor() = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
)
@Composable
fun topBarOnDrawerColor() = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
)
@@ -188,6 +188,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
TopAppBar(
title = { Text(text = stringResource(id = R.string.collections)) },
windowInsets = EmptyWindowInsets,
colors = topBarOnDrawerColor(),
)
val scope = currentRecomposeScope
LaunchedEffect(Unit) {
@@ -219,6 +220,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) =
Settings.recentFavCat = newCat
launch { sheetState.close() }
},
colors = listItemOnDrawerColor(),
)
}
}
@@ -26,7 +26,7 @@ import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ImageSearch
import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
@@ -84,6 +84,8 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.asGalleryDetail
import com.hippo.ehviewer.client.data.findBaseInfo
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.dao.DownloadInfo
@@ -100,6 +102,7 @@ import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.destinations.GalleryCommentsScreenDestination
import com.hippo.ehviewer.ui.getFavoriteIcon
import com.hippo.ehviewer.ui.jumpToReaderByPage
import com.hippo.ehviewer.ui.main.ArchiveList
import com.hippo.ehviewer.ui.main.EhPreviewItem
import com.hippo.ehviewer.ui.main.GalleryCommentCard
import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
@@ -128,6 +131,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.flattenForEach
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -524,18 +528,55 @@ fun BelowHeader(galleryDetail: GalleryDetail) {
}
},
)
val signInFirst = stringResource(R.string.sign_in_first)
val noArchive = stringResource(R.string.no_archives)
val downloadStarted = stringResource(R.string.download_archive_started)
val downloadFailed = stringResource(R.string.download_archive_failure)
val failureNoHath = stringResource(R.string.download_archive_failure_no_hath)
val archiveResult = remember(galleryDetail) {
async(Dispatchers.IO + Job(), CoroutineStart.LAZY) {
with(galleryDetail) {
EhEngine.getArchiveList(archiveUrl!!, gid, token)
}
}
}
fun showArchiveDialog() {
launchIO {
if (galleryDetail.apiUid < 0) {
showSnackbar(signInFirst)
} else {
runSuspendCatching {
val (archiveList, funds) = bgWork { archiveResult.await() }
if (archiveList.isEmpty()) {
showSnackbar(noArchive)
} else {
val selected = showNoButton {
ArchiveList(
funds = funds,
items = archiveList,
onItemClick = { resume(it) },
)
}
EhUtils.downloadArchive(galleryDetail, selected)
showSnackbar(downloadStarted)
}
}.onFailure {
when (it) {
is NoHAtHClientException -> showSnackbar(failureNoHath)
is EhException -> showSnackbar(it.displayString())
else -> {
logcat(it)
showSnackbar(downloadFailed)
}
}
}
}
}
}
EhIconButton(
icon = Icons.Default.ImageSearch,
text = stringResource(id = R.string.search_cover),
onClick = {
val key = galleryDetail.thumbKey!!
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
hash = key.substringAfterLast('/').substringBefore('-'),
).asDst(),
)
},
icon = Icons.Default.FolderZip,
text = stringResource(id = R.string.archive),
onClick = ::showArchiveDialog,
)
val torrentText = stringResource(R.string.torrent_count, galleryDetail.torrentCount)
val noTorrents = stringResource(R.string.no_torrents)
@@ -1,11 +1,6 @@
package com.hippo.ehviewer.ui.screen
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.collection.SieveCache
@@ -14,7 +9,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.FolderZip
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.CircularProgressIndicator
@@ -52,26 +46,20 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.findBaseInfo
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.composing
import com.hippo.ehviewer.ui.main.ArchiveList
import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.launchInVM
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.AppHelper
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.awaitActivityResult
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.displayString
@@ -82,16 +70,9 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.parcelize.Parcelize
import moe.tarsin.coroutines.runSuspendCatching
import splitties.systemservices.downloadManager
val detailCache = SieveCache<Long, GalleryDetail>(25)
@@ -154,78 +135,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
}
val archiveResult = remember(galleryInfo) {
async(Dispatchers.IO + Job(), CoroutineStart.LAZY) {
val detail = galleryInfo as GalleryDetail
EhEngine.getArchiveList(detail.archiveUrl!!, gid, token)
}
}
val failureNoHath = stringResource(R.string.download_archive_failure_no_hath)
val noArchive = stringResource(R.string.no_archives)
val downloadStarted = stringResource(R.string.download_archive_started)
val downloadFailed = stringResource(R.string.download_archive_failure)
val signInFirst = stringResource(R.string.sign_in_first)
fun showArchiveDialog() {
val galleryDetail = galleryInfo as? GalleryDetail ?: return
launchIO {
if (galleryDetail.apiUid < 0) {
showSnackbar(signInFirst)
} else {
runSuspendCatching {
val (archiveList, funds) = bgWork { archiveResult.await() }
if (archiveList.isEmpty()) {
showSnackbar(noArchive)
} else {
val selected = showNoButton {
ArchiveList(
funds = funds,
items = archiveList,
onItemClick = { resume(it) },
)
}
EhEngine.downloadArchive(gid, token, selected.res, selected.isHAtH)?.let {
val uri = Uri.parse(it)
val intent = Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(uri, "application/zip")
}
val name = "$gid-${EhUtils.getSuitableTitle(galleryDetail)}.zip"
try {
startActivity(intent)
withUIContext { addTextToClipboard(name, true) }
} catch (_: ActivityNotFoundException) {
val r = DownloadManager.Request(uri)
r.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
)
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
downloadManager.enqueue(r)
}
if (Settings.archiveMetadata) {
SpiderDen(galleryDetail).apply {
initDownloadDir()
writeComicInfo()
}
}
}
showSnackbar(downloadStarted)
}
}.onFailure {
when (it) {
is NoHAtHClientException -> showSnackbar(failureNoHath)
is EhException -> showSnackbar(it.displayString())
else -> {
logcat(it)
showSnackbar(downloadFailed)
}
}
}
}
}
}
Scaffold(
topBar = {
LargeTopAppBar(
@@ -248,12 +158,6 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
},
scrollBehavior = scrollBehavior,
actions = {
IconButton(onClick = ::showArchiveDialog) {
Icon(
imageVector = Icons.Default.FolderZip,
contentDescription = null,
)
}
IconButton(
onClick = {
AppHelper.share(implicit<MainActivity>(), galleryDetailUrl)
@@ -344,7 +248,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
onClick = {
dropdown = false
launchIO {
val downloadInfo = EhDownloadManager.getDownloadInfo(gid)
val downloadInfo = DownloadManager.getDownloadInfo(gid)
val canExport = downloadInfo?.state == DownloadInfo.STATE_FINISH
if (!canExport) {
awaitConfirmationOrCancel(
@@ -357,7 +261,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
CreateDocument("application/vnd.comicbook+zip"),
EhUtils.getSuitableTitle(info) + ".cbz",
)
val dirname = downloadInfo?.dirname
val dirname = downloadInfo.dirname
if (uri != null && dirname != null) {
val file = uri.toOkioPath()
val msg = runCatching {
@@ -255,6 +255,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
TopAppBar(
title = { Text(text = stringResource(id = R.string.toplist)) },
windowInsets = EmptyWindowInsets,
colors = topBarOnDrawerColor(),
)
toplists.forEach { (name, keyword) ->
ListItem(
@@ -267,6 +268,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
headlineContent = {
Text(text = name)
},
colors = listItemOnDrawerColor(),
)
}
}
@@ -274,6 +276,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
ProvideSideSheetContent { sheetState ->
TopAppBar(
title = { Text(text = stringResource(id = R.string.quick_search)) },
colors = topBarOnDrawerColor(),
actions = {
IconButton(onClick = {
launch {
@@ -437,6 +440,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
Icon(imageVector = Icons.Default.Reorder, contentDescription = null)
}
},
colors = listItemOnDrawerColor(),
)
}
}
@@ -175,10 +175,12 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
useSelectedAsSummary = true,
entries = languages,
)
if (BuildConfig.DEBUG) {
SwitchPreference(
var enableCronet by Settings.enableCronet.asMutableState()
if (BuildConfig.DEBUG || !enableCronet) {
SwitchPref(
checked = enableCronet,
onMutate = { enableCronet = !enableCronet },
title = "Enable Cronet",
value = Settings::enableCronet,
)
}
if (isAtLeastO) {
@@ -201,11 +203,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
title = stringResource(id = R.string.animate_items),
summary = stringResource(id = R.string.animate_items_summary),
)
SwitchPreference(
title = stringResource(id = R.string.desktop_site),
summary = stringResource(id = R.string.desktop_site_summary),
value = Settings::desktopSite,
)
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
LauncherPreference(
title = stringResource(id = R.string.settings_advanced_export_data),
@@ -64,7 +64,7 @@ open class TextActionModeCallback(
return true
}
override fun onDestroyActionMode(mode: ActionMode) {}
override fun onDestroyActionMode(mode: ActionMode) = Unit
override fun onGetContentRect(mode: ActionMode, view: View, outRect: PlatformRect) {
outRect.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt())
@@ -56,9 +56,7 @@ fun VerticalScrollbar(
adapter = adapter,
modifier = scrollbarModifier
.padding(
start = HorizontalPadding,
top = contentPadding.calculateTopPadding(),
end = HorizontalPadding,
bottom = contentPadding.calculateBottomPadding(),
)
.graphicsLayer { alpha = scrollbarAlpha.value },
@@ -70,13 +68,14 @@ fun VerticalScrollbar(
fun scrollbarStyle(color: Color) = ScrollbarStyle(
minimalHeight = ThumbLength,
thickness = ThumbThickness,
hoverDurationMillis = 300,
padding = ThumbPadding,
shape = ThumbShape,
hoverDurationMillis = 300,
unhoverColor = color,
hoverColor = color,
)
private val HorizontalPadding = 8.dp
private val ThumbPadding = 8.dp
private val ThumbLength = 48.dp
private val ThumbThickness = 8.dp
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)
@@ -7,6 +7,8 @@ import com.hippo.ehviewer.client.executeAndParseAs
import com.hippo.ehviewer.client.executeSafely
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.ensureSuccess
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
@@ -66,7 +68,11 @@ object AppUpdater {
return null
}
suspend fun downloadUpdate(url: String, file: File) = ghStatement(url).executeSafely { response ->
suspend fun downloadUpdate(url: String, file: File) = ghStatement(url) {
timeout {
requestTimeoutMillis = 60_000
}
}.executeSafely { response ->
response.status.ensureSuccess()
if (url.endsWith("zip")) {
response.bodyAsChannel().toInputStream().use { stream ->
@@ -83,8 +89,12 @@ object AppUpdater {
}
}
private suspend inline fun ghStatement(url: String) = ktorClient.prepareGet(url) {
private suspend inline fun ghStatement(
url: String,
builder: HttpRequestBuilder.() -> Unit = {},
) = ktorClient.prepareGet(url) {
header("Authorization", GithubTokenParts.joinToString("_", prefix = "Bearer "))
apply(builder)
}
private val GithubTokenParts = arrayOf(
@@ -29,15 +29,13 @@ import com.hippo.ehviewer.ui.MainActivity
import splitties.systemservices.clipboardManager
fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
clipboardManager.apply {
setPrimaryClip(
ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
}
},
)
}
clipboardManager.setPrimaryClip(
ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
}
},
)
}
fun Context.addTextToClipboard(text: CharSequence?, useToast: Boolean = false) {
@@ -3,6 +3,7 @@ package com.hippo.ehviewer.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
/**
* Find the closest Activity in a given Context.
@@ -15,3 +16,10 @@ inline fun <reified T : Activity> Context.findActivity(): T {
}
throw IllegalStateException("findActivity() should be called in the context of an Activity")
}
fun Context.restartApplication() {
packageManager.getLaunchIntentForPackage(packageName)?.let {
startActivity(Intent.makeRestartActivityTask(it.component))
Runtime.getRuntime().exit(0)
}
}
@@ -28,7 +28,7 @@ object FileUtils {
// ext4 through a FUSE layer, so use that limit.
private const val MAX_FILENAME_BYTES = 255
fun ensureDirectory(file: File?) = file?.let { if (it.exists()) it.isDirectory else it.mkdirs() } ?: false
fun ensureDirectory(file: File?) = file?.let { if (it.exists()) it.isDirectory else it.mkdirs() } == true
/**
* Convert byte to human readable string.<br></br>
@@ -8,6 +8,6 @@ import splitties.init.appCtx
fun detectAds(index: Int, size: Int) = index > size - 10 && Settings.stripExtraneousAds.value
val GalleryInfo.hasAds
get() = simpleTags?.any { "extraneous ads" in it } ?: false
get() = simpleTags?.any { "extraneous ads" in it } == true
val AdsPlaceholderFile = appCtx.filesDir.toOkioPath() / "AdsPlaceholder"
@@ -12,4 +12,4 @@ fun String.toLongOrDefault(defaultValue: Long): Long = toLongOrNull() ?: default
fun String.toFloatOrDefault(defaultValue: Float): Float = toFloatOrNull() ?: defaultValue
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) == true
@@ -2,12 +2,12 @@ package com.hippo.ehviewer.util
import android.annotation.SuppressLint
import android.webkit.WebView
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ktor.CHROME_USER_AGENT
@SuppressLint("SetJavaScriptEnabled")
fun WebView.setDefaultSettings() = with(settings) {
builtInZoomControls = true
displayZoomControls = false
javaScriptEnabled = true
userAgentString = Settings.userAgent
userAgentString = CHROME_USER_AGENT
}
@@ -2,6 +2,7 @@ package com.hippo.files
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsContract.Document
@@ -32,7 +33,13 @@ class AndroidFileSystem(context: Context) : FileSystem() {
source.runCatching {
DocumentsContract.renameDocument(contentResolver, toUri(), target.name)
}.getOrNull() ?: throw IOException("Failed to move $source to $target")
}.onFailure {
// ExternalStorageProvider always throw exception when renameDocument on API 28
// https://android.googlesource.com/platform/frameworks/base/+/7bf90408e36613a84dc2a665905fde2c83cfa797
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
throw IOException("Failed to move $source to $target")
}
}
}
override fun canonicalize(path: Path): Path {
@@ -126,7 +133,7 @@ class AndroidFileSystem(context: Context) : FileSystem() {
dir / displayName
}
}
}.getOrElse { if (throwOnFailure) throw it else null }
}.getOrElse { if (throwOnFailure) throw IOException("Failed to list $dir") else null }
}
override fun metadataOrNull(path: Path): FileMetadata? {
@@ -180,16 +187,16 @@ class AndroidFileSystem(context: Context) : FileSystem() {
return ParcelFileDescriptor.open(path.toFile(), ParcelFileDescriptor.parseMode(mode))
}
if ('w' in mode && !exists(path)) {
path.parent?.runCatching {
return runCatching {
if ('w' in mode && !exists(path)) {
val parent = path.parent ?: return@runCatching null
val displayName = path.name
val extension = displayName.substringAfterLast('.', "").ifEmpty { null }?.lowercase()
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream"
DocumentsContract.createDocument(contentResolver, toUri(), mimeType, displayName)
}?.getOrNull() ?: throw IOException("Failed to open file: $path")
}
return contentResolver.openFileDescriptor(path.toUri(), mode) ?: throw IOException("Failed to open file: $path")
DocumentsContract.createDocument(contentResolver, parent.toUri(), mimeType, displayName)
}
contentResolver.openFileDescriptor(path.toUri(), mode)
}.getOrNull() ?: throw IOException("Failed to open file: $path")
}
}
+1 -8
View File
@@ -92,9 +92,6 @@
<string name="no_comments">Kein Kommentar</string>
<string name="no_more_comments">Keine weiteren Kommentare</string>
<string name="more_comment">Weitere Kommentare</string>
<string name="no_previews">Keine Vorschau</string>
<string name="no_more_previews">Keine weiteren Vorschauen</string>
<string name="more_previews">Weitere Vorschauen</string>
<string name="refresh">Neu laden</string>
<string name="open_in_other_app">Öffnen mit …</string>
<string name="rate_successfully">Bewertung erfolgreich</string>
@@ -159,7 +156,6 @@
<string name="cancel_vote_down_successfully">Die negative Stimmung wurde abgebrochen</string>
<string name="vote_failed">Stimmung gescheitert</string>
<string name="check_vote_status">Details der Stimmungen anzeigen</string>
<string name="gallery_previews">Vorschauen der Galerie</string>
<string name="go_to">Gehe zu</string>
<string name="scene_download_title">Download - %s</string>
<string name="no_download_info">Downloads werden hier angezeigt</string>
@@ -231,15 +227,11 @@
<string name="settings_eh_detail_size">Größe der Details</string>
<string name="settings_eh_detail_size_long">Lange</string>
<string name="settings_eh_detail_size_short">Kurz</string>
<string name="settings_eh_thumb_resolution">Auflösung der Miniaturbilder</string>
<string name="settings_eh_thumb_resolution_summary">Nun %s. Mit abnormale Auflösung kann die Miniaturbilder nicht normalerweise geladen werden.</string>
<string name="settings_eh_thumb_resolution_auto">Automatisch</string>
<string name="settings_advanced_app_language_title">Sprache der App (Language)</string>
<string name="torrent_count">Torrent (%d)</string>
<string name="settings_eh_show_jpn_title">Japanische Titel anzeigen</string>
<string name="settings_eh_filter">Galerie-Blocker</string>
<string name="settings_eh_filter_summary">Nach Titel, Uploader order Etiketten die unerwünschten Galerien verstecken</string>
<string name="settings_read_volume_page">Seiten mit Lautstärketaste umblättern</string>
<string name="settings_download">Download</string>
<string name="settings_download_download_location">Downloadziel</string>
<string name="settings_download_cant_get_download_location">Downloadziel konnte nicht geladen werden</string>
@@ -286,6 +278,7 @@
<string name="just_now">Gerade</string>
<string name="yesterday">Gestern</string>
<string name="some_days_ago">Vor %d Tagen</string>
<string name="archive">Archivdateien</string>
<string name="no_archives">Keine Archivdatei</string>
<string name="download_archive_started">Archivdatei wird heruntergeladen</string>
<string name="download_archive_failure">Fehler beim Herunterladen der Archivdatei</string>
+1 -9
View File
@@ -114,9 +114,6 @@
<string name="no_comments">Sin comentarios</string>
<string name="no_more_comments">No hay más comentarios</string>
<string name="more_comment">Más comentarios</string>
<string name="no_previews">Sin vistas previas</string>
<string name="no_more_previews">No hay más vistas previas</string>
<string name="more_previews">Más vistas previas</string>
<string name="refresh">Actualizar</string>
<string name="open_in_other_app">Abrir en otra aplicación</string>
<string name="rate_successfully">Valoración completada</string>
@@ -181,7 +178,6 @@
<string name="check_vote_status">Verificar estado del voto</string>
<!-- Gallery Previews -->
<string name="gallery_previews">Vista previa de la galería</string>
<string name="go_to">Ir a</string>
<!-- Download -->
@@ -251,9 +247,6 @@
<string name="settings_eh_detail_size">Tamaño del detalle</string>
<string name="settings_eh_detail_size_long">Largo</string>
<string name="settings_eh_detail_size_short">Corto</string>
<string name="settings_eh_thumb_resolution">Resolución de vista previa</string>
<string name="settings_eh_thumb_resolution_summary">%s, las resoluciones especiales podrían causar errores en la vista previa</string>
<string name="settings_eh_thumb_resolution_auto">Automático</string>
<string name="settings_eh_show_jpn_title">Mostrar títulos en japonés</string>
<string name="settings_eh_show_jpn_title_summary">Es necesario habilitar los títulos en japonés en la propia página de EHentai</string>
<string name="settings_eh_show_gallery_pages">Mostrar número de páginas de la galería</string>
@@ -261,8 +254,6 @@
<string name="settings_eh_filter">Filtrado de galerías</string>
<string name="settings_eh_filter_summary">Filtrado de galerías por título, uploader y etiquetas. Para más detalles presiona el ícono (i) en la parte superior del menú</string>
<string name="settings_read_volume_page">Pasar páginas con botón del volumen</string>
<string name="settings_download">Descargas</string>
<string name="settings_download_download_location">Ruta de descargas</string>
<string name="settings_download_cant_get_download_location">No se puede obtener ruta de descargas</string>
@@ -341,6 +332,7 @@
<string name="just_now">Ahora</string>
<string name="yesterday">Ayer</string>
<string name="some_days_ago">Hace %d días atrás</string>
<string name="archive">Archivo</string>
<string name="no_archives">Sin archivos</string>
<string name="download_archive_started">Descarga de archivos iniciada</string>
<string name="download_archive_failure">Error al descargar archivo</string>
+1 -9
View File
@@ -123,9 +123,6 @@
<string name="no_comments">Aucun commentaire</string>
<string name="no_more_comments">Pas d\'autres commentaires</string>
<string name="more_comment">Voir plus de commentaires</string>
<string name="no_previews">Aucun aperçu</string>
<string name="no_more_previews">Pas d\'autres aperçus</string>
<string name="more_previews">Voir plus d\'aperçus</string>
<string name="refresh">Rafraîchir</string>
<string name="open_in_other_app">Ouvrir avec une autre appli</string>
<string name="rate_successfully">Noté avec succès</string>
@@ -196,7 +193,6 @@
<string name="check_vote_status">Voir les détails des votes</string>
<!-- Gallery Previews -->
<string name="gallery_previews">Aperçus de la galerie</string>
<string name="go_to">Aller à</string>
<!-- Download -->
@@ -286,15 +282,10 @@
<string name="settings_eh_detail_size">Taille des détails</string>
<string name="settings_eh_detail_size_long">Long</string>
<string name="settings_eh_detail_size_short">Court</string>
<string name="settings_eh_thumb_resolution">Résolution des miniatures</string>
<string name="settings_eh_thumb_resolution_summary">Auctuellement %s. Certaines résolutions spéciales êmpechent de les charger correctement.</string>
<string name="settings_eh_thumb_resolution_auto">Auto</string>
<string name="settings_eh_show_jpn_title">Montrer les titres japonais</string>
<string name="settings_eh_filter">Bloqueur des galeries</string>
<string name="settings_eh_filter_summary">Filtrer les galerie par les titres, les uploaders ou les tags</string>
<string name="settings_read_volume_page">Tourner la page avec les Volumes</string>
<!-- My Tags Activity -->
<string name="my_tags">Tags suivis</string>
@@ -381,6 +372,7 @@
<string name="just_now">En ce moment</string>
<string name="yesterday">Hier</string>
<string name="some_days_ago">Il y a %d jours</string>
<string name="archive">Fiches archives</string>
<string name="no_archives">Aucun fiche archives</string>
<string name="download_archive_started">Téléchargement du fiche archives a commencé</string>
<string name="download_archive_failure">Téléchargement du fiche archives a échoué</string>
+27 -39
View File
@@ -33,15 +33,15 @@
<string name="text_is_empty">テキストを入力してください</string>
<string name="waring">警告</string>
<string name="invalid_download_location">ダウンロードパスは現在利用できません。ダウンロードパスを設定してください。</string>
<string name="clipboard_gallery_url_snack_message">クリップボードにはギャラリーURLが検出されました。</string>
<string name="clipboard_gallery_url_snack_action">移動</string>
<string name="clipboard_gallery_url_snack_message">クリップボードからギャラリーの URL が検出されました。</string>
<string name="clipboard_gallery_url_snack_action">表示</string>
<string name="error_timeout">タイムアウト</string>
<string name="error_unknown_host">不明なホスト</string>
<string name="error_redirection">リダイレクトが多すぎます</string>
<string name="error_socket">ネットワークエラー</string>
<string name="error_unknown">不明なエラー</string>
<string name="error_cant_find_activity">アクティビティが見つかりません。</string>
<string name="error_cannot_parse_the_url">リンクを解析できません。</string>
<string name="error_cannot_parse_the_url">リンクを解析できません</string>
<string name="error_decoding_failed">デコードに失敗しました</string>
<string name="error_reading_failed">読み込めませんでした</string>
<string name="error_out_of_range">範囲外</string>
@@ -57,7 +57,7 @@
<string name="error_password_cannot_empty">パスワードは空欄にできません</string>
<string name="guest_mode">ゲストモード</string>
<string name="sign_in_failed">ログインに失敗しました</string>
<string name="sign_in_failed_tip">この問題が続く場合は、「%s」をお試しください。</string>
<string name="sign_in_failed_tip">この問題が続く場合は「%s」をお試しください。</string>
<string name="get_it">了解</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">ExHentai を検索</string>
@@ -93,11 +93,11 @@
<string name="add_quick_search_tip">ギャラリー検索結果の設定などはクイック検索として保存されます。検索の設定などを保存するには、まず検索を実行してください。</string>
<string name="readme">README</string>
<string name="image_search_not_quick_search">画像検索をクイック検索として追加できませんでした</string>
<string name="duplicate_quick_search">「%s」というクイック検索は既に存在しています。</string>
<string name="duplicate_name">この名前は既に使用されています。</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>
<string name="go_to_hint">ページ: %1$d - 合計: %2$d ページ</string>
<!-- search_min_rating -->
<string name="any">すべて</string>
<string name="star_2">2 つ星</string>
@@ -108,7 +108,7 @@
<string name="download">ダウンロード</string>
<string name="read">読む</string>
<string name="favored_times">\u2665 %d</string>
<string name="rating_text">%1$s (%2$.2f, %3$d)</string>
<string name="rating_text">%1$s (%2$.2f - %3$d)</string>
<string name="torrent_count">Torrent (%d)</string>
<string name="share">共有</string>
<string name="rate">評価</string>
@@ -118,9 +118,6 @@
<string name="no_comments">コメントなし</string>
<string name="no_more_comments">コメントはこれ以上ありません</string>
<string name="more_comment">他のコメントを見る</string>
<string name="no_previews">プレビューなし</string>
<string name="no_more_previews">プレビューはこれ以上ありません</string>
<string name="more_previews">他のプレビューを見る</string>
<string name="refresh">更新</string>
<string name="view_original">オリジナルの画像を表示</string>
<string name="open_in_other_app">他のアプリで開く</string>
@@ -206,7 +203,6 @@
<string name="format_url">URL</string>
<string name="format_plain">プレーンテキスト</string>
<!-- Gallery Previews -->
<string name="gallery_previews">ギャラリーのプレビュー</string>
<string name="go_to">ページに移動</string>
<!-- Download -->
<string name="scene_download_title">ダウンロード - %s</string>
@@ -221,11 +217,11 @@
<string name="stat_509_alert_title">509 アラート</string>
<string name="stat_509_alert_text">ダウンロード制限に達しました。しばらくしてからもう一度やり直してください。</string>
<string name="stat_download_done_title">ダウンロード完了</string>
<string name="stat_download_done_text_succeeded">%d 件のダウンロードが完了済み</string>
<string name="stat_download_done_text_failed">%d 件のダウンロードに失敗しました</string>
<string name="stat_download_done_text_mix">%1$d 件のダウンロードが完了済み、%2$d 件が失敗</string>
<string name="stat_download_done_line_succeeded">ダウンロード完了: %s</string>
<string name="stat_download_done_line_failed">ダウンロード失敗: %s</string>
<string name="stat_download_done_text_succeeded">%d 件が成功</string>
<string name="stat_download_done_text_failed">%d 件が失敗</string>
<string name="stat_download_done_text_mix">%1$d 件のダウンロードが成功、%2$d 件が失敗</string>
<string name="stat_download_done_line_succeeded">成功: %s</string>
<string name="stat_download_done_line_failed">失敗: %s</string>
<string name="download_remove_dialog_title">ダウンロードを削除</string>
<string name="download_remove_dialog_message">ダウンロードリストから「%s」を削除しますか?</string>
<string name="download_remove_dialog_message_2">ダウンロードリストから %d 件のタスクを削除しますか?</string>
@@ -240,7 +236,7 @@
<string name="reset_reading_progress_message">すべてのダウンロード済みのギャラリーの閲覧進捗をリセットしますか?</string>
<string name="download_service_label">EhViewer ダウンロードサービス</string>
<string name="download_speed_text">%s</string>
<string name="download_speed_text_2">%1$s - 残り %2$s</string>
<string name="download_speed_text_2">%1$s - 残り: %2$s</string>
<string name="remember_download_label">ダウンロードラベルを記憶</string>
<string name="default_download_label">デフォルトのダウンロードラベル</string>
<string name="added_to_download_list">ダウンロードリストに追加しました</string>
@@ -253,7 +249,7 @@
<string name="new_label_title">ラベルを作成</string>
<string name="label_text_is_empty">ラベルテキストを入力してください</string>
<string name="label_text_is_invalid">「デフォルト」は無効なラベルです</string>
<string name="label_text_exist">ラベルは既に存在しています</string>
<string name="label_text_exist">ラベルはすでに存在しています</string>
<string name="rename_label_title">ラベルの名前を変更</string>
<string name="delete_label">「%s」のラベルを削除しますか?</string>
<!-- History -->
@@ -285,9 +281,9 @@
<string name="image_saved">画像は「%s」に保存されました</string>
<string name="settings_eh">EH</string>
<string name="settings_eh_sign_out">ログアウト</string>
<string name="settings_eh_identity_cookies_signed">下記の情報は誰でもこのアカウントにログインできますので&lt;br&gt;&lt;b&gt;絶対に他人に渡さないように!&lt;/b&gt;</string>
<string name="settings_eh_identity_cookies_signed">このアカウントは Identity Cookie を使用してログインできます。&lt;br&gt;&lt;b&gt;これは安全に保管してください&lt;/b&gt;</string>
<string name="settings_eh_identity_cookies_guest">ゲストモード</string>
<string name="settings_eh_clear_igneous">アイジニアスを消去</string>
<string name="settings_eh_clear_igneous">Igneous を消去</string>
<string name="settings_u_config">E-Hentai の設定</string>
<string name="settings_u_config_summary">E-Hentai ウェブサイト内の設定をします</string>
<string name="settings_my_tags">マイタグ</string>
@@ -301,10 +297,7 @@
<string name="settings_eh_detail_size_long">長い</string>
<string name="settings_eh_detail_size_short">短い</string>
<string name="settings_eh_thumb_columns">サムネイルのカラム数</string>
<string name="settings_eh_thumb_resolution">サムネイル解像度</string>
<string name="settings_eh_thumb_resolution_summary">現在の解像度は「%s」です。解像度によってはサムネイルの取得に失敗する場合があります。</string>
<string name="settings_eh_thumb_resolution_auto">自動</string>
<string name="settings_eh_force_eh_thumb">E-hentai サムネイルサーバーを使用する</string>
<string name="settings_eh_force_eh_thumb">E-Hentai サムネイルサーバーを使用する</string>
<string name="settings_eh_force_eh_thumb_summary">サムネイルの読み込みに問題がある場合は無効化してください</string>
<string name="settings_eh_show_jpn_title">日本語のタイトルを表示</string>
<string name="settings_eh_show_jpn_title_summary">E-Hentai ウェブサイトの設定で日本語のタイトルを有効化する必要があります</string>
@@ -319,19 +312,15 @@
<string name="settings_eh_tag_translations_source">プレースホルダー</string>
<string name="settings_eh_tag_translations_source_url">https://placeholder</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_volume_page_fast">高速</string>
<string name="settings_read_volume_page_slow">低速</string>
<string name="settings_read_reverse_volume">音量ボタンを入れ替える</string>
<string name="settings_eh_filter_summary">タイトル、アップローダー、タグ、コメントの投稿者またはギャラリーをブロックします</string>
<string name="settings_block_extraneous_ads">[試験的] 関係のない広告をブロック</string>
<string name="settings_ads_placeholder">[任意] 広告を置換するプレースホルダーを選択</string>
<string name="settings_download">ダウンロード</string>
<string name="settings_download_download_location">ダウンロードパス</string>
<string name="settings_download_cant_get_download_location">ダウンロードパスを取得できない</string>
<string name="settings_download_download_location">ダウンロード先</string>
<string name="settings_download_cant_get_download_location">ダウンロード先を取得できません</string>
<string name="settings_download_media_scan">メディアスキャンを許可する</string>
<string name="settings_download_media_scan_summary_on">ギャラリーアプリで他の人に見せないようにします</string>
<string name="settings_download_media_scan_summary_off">ほとんどのギャラリーアプリでダウンロードパスを無視します</string>
<string name="settings_download_media_scan_summary_off">ほとんどのギャラリーアプリでダウンロード先のパスを無視します</string>
<string name="settings_download_concurrency">ダウンロードのスレッド数</string>
<string name="settings_download_concurrency_summary">同時に最大 %s 枚の画像をダウンロードします</string>
<string name="settings_download_download_delay">ダウンロードの遅延</string>
@@ -366,7 +355,7 @@
<string name="settings_advanced_dump_logcat_summary">Logcat のログを内部ストレージに保存します</string>
<string name="settings_advanced_dump_logcat_failed">Logcat のダンプに失敗しました</string>
<string name="settings_advanced_dump_logcat_to">Logcat のログが「%s」にダンプされました</string>
<string name="settings_advanced_read_cache_size">閲覧キャッシュのサイズ</string>
<string name="settings_advanced_read_cache_size">読書用キャッシュのサイズ</string>
<string name="settings_advanced_app_language_title">アプリの言語</string>
<string name="settings_advanced_hardware_bitmap_threshold">ハードウェアビットマップ (パフォーマンス向上) のしきい値</string>
<string name="settings_advanced_hardware_bitmap_threshold_summary">長い画像の読み込みに失敗する場合はこれを減らしてみてください</string>
@@ -420,6 +409,7 @@
<string name="just_now">たった今</string>
<string name="yesterday">昨日</string>
<string name="some_days_ago">%d 日前</string>
<string name="archive">圧縮パッケージ</string>
<string name="no_archives">アーカイブなし</string>
<string name="download_archive_started">アーカイブのダウンロードを開始しました</string>
<string name="download_archive_failure">アーカイブをダウンロードできませんでした</string>
@@ -427,8 +417,8 @@
<string name="archive_original">オリジナル</string>
<string name="archive_resample">リサンプル</string>
<string name="download_archive_failure_no_hath">アーカイブのダウンロードは H@H クライアントが必要です</string>
<string name="current_funds">現在の Fund:</string>
<string name="insufficient_funds">Fund が不足しています</string>
<string name="current_funds">現在の資金:</string>
<string name="insufficient_funds">資金が不足しています</string>
<string name="image_limits">画像の制限</string>
<string name="image_limits_summary">使用中: </string>
<string name="image_limits_normal">制限なし</string>
@@ -437,7 +427,7 @@
<string name="reset">リセット</string>
<string name="settings_privacy">プライバシー</string>
<string name="settings_privacy_secure">スクリーンショットを抑制する</string>
<string name="settings_privacy_secure_summary">アプリのコンテンツがスクリーンショットで撮影されたり「最近使用したアプリ」のリストに表示されないようにします。</string>
<string name="settings_privacy_secure_summary">アプリのコンテンツがスクリーンショットで撮影されたり「最近使用したアプリ」のリストに表示されないようにします</string>
<string name="clear_search_history">デバイスの検索履歴を消去</string>
<string name="clear_search_history_summary">このデバイスから検索履歴を消去します</string>
<string name="clear_search_history_confirm">検索履歴を消去しますか?</string>
@@ -504,8 +494,6 @@
<string name="preload_thumb_aggressively">サムネイルを積極的にプリロードする</string>
<string name="animate_items">リスト項目のアニメーション</string>
<string name="animate_items_summary">クラッシュやフレームドロップが発生する場合はこれを無効化してください</string>
<string name="desktop_site">デスクトップのサイト</string>
<string name="desktop_site_summary">デフォルトの画像解像度を高くします</string>
<string name="auto_updates">自動で更新を確認する</string>
<string name="update_frequency_never">しない</string>
<string name="update_frequency_daily">毎日</string>
+1 -9
View File
@@ -116,9 +116,6 @@
<string name="no_comments">코멘트 없음</string>
<string name="no_more_comments">코멘트가 더 없음</string>
<string name="more_comment">코멘트 더 보기</string>
<string name="no_previews">미리 보기 없음</string>
<string name="no_more_previews">미리 보기가 더 없음</string>
<string name="more_previews">더 불러오기</string>
<string name="refresh">새로고침</string>
<string name="open_in_other_app">다른 앱에서 열기</string>
<string name="rate_successfully">평가를 남겼습니다.</string>
@@ -173,7 +170,6 @@
<string name="check_vote_status">추천/비추천 상태 보기</string>
<!-- Gallery Previews -->
<string name="gallery_previews">갤러리 미리 보기</string>
<string name="go_to">이동</string>
<!-- Download -->
@@ -258,9 +254,6 @@
<string name="settings_eh_detail_size">상세 정보 크기</string>
<string name="settings_eh_detail_size_long">길게</string>
<string name="settings_eh_detail_size_short">짧게</string>
<string name="settings_eh_thumb_resolution">섬네일 해상도</string>
<string name="settings_eh_thumb_resolution_summary">특정 해상도로 설정하면 섬네일을 불러오지 못할 수 있습니다. 현재 설정\u003a \u005b%s\u005d</string>
<string name="settings_eh_thumb_resolution_auto">자동</string>
<string name="settings_eh_show_jpn_title">일본어 제목 표시</string>
<string name="settings_eh_show_gallery_pages">갤러리 페이지 수 표시</string>
<string name="settings_eh_show_gallery_pages_summary">갤러리 목록에 해당 갤러리의 페이지 수를 표시합니다</string>
@@ -269,8 +262,6 @@
<string name="settings_eh_tag_translations_source">Placeholder</string>
<string name="settings_eh_tag_translations_source_url">https://placeholder</string>
<string name="settings_read_volume_page">볼륨 키로 페이지 넘기기</string>
<string name="settings_download">다운로드</string>
<string name="settings_download_download_location">다운로드 위치</string>
<string name="settings_download_cant_get_download_location">다운로드 위치를 가져올 수 없습니다.</string>
@@ -354,6 +345,7 @@
<string name="just_now">방금</string>
<string name="yesterday">어제</string>
<string name="some_days_ago">%d일 전</string>
<string name="archive">아카이브</string>
<string name="no_archives">아카이브 없음</string>
<string name="download_archive_started">아카이브 다운로드가 시작되었습니다.</string>
<string name="download_archive_failure">아카이브 다운로드에 실패했습니다.</string>
@@ -45,12 +45,9 @@
<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>
@@ -121,7 +118,6 @@
<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>
@@ -175,7 +171,6 @@
<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="app_language_system">Systemspråk (forvalg)</string>
<string name="error_something_wrong_happened">Noe gikk galt</string>
<string name="just_now">Akkurat nå</string>
+1 -9
View File
@@ -118,9 +118,6 @@
<string name="no_comments">ไม่มีความคิดเห็น</string>
<string name="no_more_comments">ไม่มีความเห็นเพิ่มเติมแล้ว</string>
<string name="more_comment">ความคิดเห็นเพิ่มเติม</string>
<string name="no_previews">ไม่มีรูปตัวอย่าง</string>
<string name="no_more_previews">ไม่มีรูปตัวอย่างเพิ่มเติมอีกแล้ว</string>
<string name="more_previews">มีรูปตัวอย่าง</string>
<string name="refresh">รีเฟรช</string>
<string name="open_in_other_app">เปิดด้วยแอปอื่น</string>
<string name="rate_successfully">ให้คะแนนเรียบร้อยแล้ว</string>
@@ -188,7 +185,6 @@
<string name="check_vote_status">ดูรายละเอียดการโหวต</string>
<!-- Gallery Previews -->
<string name="gallery_previews">ตัวอย่างแกลเลอรี่</string>
<string name="go_to">ไปที่</string>
<!-- Download -->
@@ -273,9 +269,6 @@
<string name="settings_eh_detail_size">ขนาดของรายละเอียด</string>
<string name="settings_eh_detail_size_long">อย่างยาว</string>
<string name="settings_eh_detail_size_short">อย่างสั้น</string>
<string name="settings_eh_thumb_resolution">ความละเอียดภาพตัวอย่าง</string>
<string name="settings_eh_thumb_resolution_summary">ค่าตอนนี้เป็น %s. การตั้งความละเอียดที่แน่นอนอาจจะส่งผลต่อการโหลดตัวอย่างที่อาจจะไม่สำเร็จด้วย</string>
<string name="settings_eh_thumb_resolution_auto">อัตโนมัติ</string>
<string name="settings_eh_show_jpn_title">แสดงชื่อเรื่องญี่ปุ่น</string>
<string name="settings_eh_show_gallery_pages">แสดงจำนวนหน้าของแกลเลอรี่</string>
<string name="settings_eh_show_gallery_pages_summary">แสดงจำนวนหน้าที่มีอยู่ในแต่ละแกลเลอรี่</string>
@@ -284,8 +277,6 @@
<string name="settings_eh_tag_translations_source">Placeholder</string>
<string name="settings_eh_tag_translations_source_url">https://placeholder</string>
<string name="settings_read_volume_page">ใช้ปุ่มปรับเสียงเพื่อเปลี่ยนหน้า</string>
<string name="settings_download">การดาวน์โหลด</string>
<string name="settings_download_download_location">ตำแหน่งแหล่งดาวน์โหลด</string>
<string name="settings_download_cant_get_download_location">ไม่สามารถเข้าถึงแหล่งดาวน์โหลดได้</string>
@@ -369,6 +360,7 @@
<string name="just_now">เดี๋ยวนี้</string>
<string name="yesterday">เมื่อวาน</string>
<string name="some_days_ago">%d วันที่ผ่านมา</string>
<string name="archive">การบีบอัด</string>
<string name="no_archives">ยังไม่มีไฟล์บีบอัด</string>
<string name="download_archive_started">เริ่มการดาวน์โหลดบีบอัด</string>
<string name="download_archive_failure">การดาวน์โหลดไฟล์บีบอัดล้มเหลว</string>
+4 -16
View File
@@ -114,9 +114,6 @@
<string name="no_comments">暂无评论</string>
<string name="no_more_comments">已显示所有评论</string>
<string name="more_comment">查看更多评论</string>
<string name="no_previews">没有预览</string>
<string name="no_more_previews">已显示所有预览</string>
<string name="more_previews">查看更多预览</string>
<string name="refresh">刷新</string>
<string name="view_original">查看原图</string>
<string name="open_in_other_app">在其他应用中打开</string>
@@ -202,7 +199,6 @@
<string name="format_url">链接</string>
<string name="format_plain">纯文本</string>
<!-- Gallery Previews -->
<string name="gallery_previews">画廊预览</string>
<string name="go_to">跳页</string>
<!-- Download -->
<string name="scene_download_title">下载 - %s</string>
@@ -298,9 +294,6 @@
<string name="settings_eh_detail_size_long">长</string>
<string name="settings_eh_detail_size_short">短</string>
<string name="settings_eh_thumb_columns">缩略图列数</string>
<string name="settings_eh_thumb_resolution">缩略图分辨率</string>
<string name="settings_eh_thumb_resolution_summary">%s,指定分辨率可能使获取缩略图失败</string>
<string name="settings_eh_thumb_resolution_auto">自动</string>
<string name="settings_eh_force_eh_thumb">使用 e-hentai 缩略图服务器</string>
<string name="settings_eh_force_eh_thumb_summary">若缩略图加载失败可尝试关闭此项</string>
<string name="settings_eh_show_jpn_title">显示日文标题</string>
@@ -317,10 +310,6 @@
<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_read_volume_page">使用音量键翻页</string>
<string name="settings_read_volume_page_fast">快</string>
<string name="settings_read_volume_page_slow">慢</string>
<string name="settings_read_reverse_volume">翻转音量键</string>
<string name="settings_block_extraneous_ads">[实验性] 屏蔽外部广告</string>
<string name="settings_ads_placeholder">[可选] 选择替换外部广告的图片</string>
<string name="settings_download">下载</string>
@@ -414,12 +403,13 @@
<string name="just_now">刚刚</string>
<string name="yesterday">昨天</string>
<string name="some_days_ago">%d 天前</string>
<string name="no_archives">没有压缩包</string>
<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="no_archives">没有压缩包</string>
<string name="download_archive_started">开始下载压缩包</string>
<string name="download_archive_failure">无法下载压缩包</string>
<string name="download_archive_failure_no_hath">下载压缩包需要 H@H 客户端</string>
<string name="current_funds">当前资金:</string>
<string name="insufficient_funds">余额不足</string>
@@ -500,8 +490,6 @@
<string name="preload_thumb_aggressively">激进地预载缩略图</string>
<string name="animate_items">列表项目动画</string>
<string name="animate_items_summary">如遇崩溃/掉帧请尝试禁用此项</string>
<string name="desktop_site">桌面版网站</string>
<string name="desktop_site_summary">更高的默认图片分辨率</string>
<string name="auto_updates">自动检查更新</string>
<string name="update_frequency_never">从不</string>
<string name="update_frequency_daily">每天</string>
+1 -9
View File
@@ -129,9 +129,6 @@
<string name="no_comments">暫無評論</string>
<string name="no_more_comments">已顯示所有評論</string>
<string name="more_comment">查看更多評論</string>
<string name="no_previews">沒有預覽</string>
<string name="no_more_previews">已顯示所有預覽</string>
<string name="more_previews">查看更多預覽</string>
<string name="refresh">刷新</string>
<string name="open_in_other_app">在其他應用中打開</string>
<string name="rate_successfully">評分成功</string>
@@ -207,7 +204,6 @@
<string name="last_edited">上次修改時間:%s</string>
<!-- Gallery Previews -->
<string name="gallery_previews">畫廊預覽</string>
<string name="go_to">跳頁</string>
<!-- Download -->
@@ -305,9 +301,6 @@
<string name="settings_eh_detail_size_long">長</string>
<string name="settings_eh_detail_size_short">短</string>
<string name="settings_eh_thumb_columns">縮略圖列數</string>
<string name="settings_eh_thumb_resolution">縮略圖分辨率</string>
<string name="settings_eh_thumb_resolution_summary">%s,指定分辨率可能使獲取縮略圖失敗</string>
<string name="settings_eh_thumb_resolution_auto">自動</string>
<string name="settings_eh_show_jpn_title">顯示日文標題</string>
<string name="settings_eh_show_jpn_title_summary">需同時在 EHentai 網站設置中啟用 Japanese Title</string>
<string name="settings_eh_show_gallery_pages">顯示畫廊頁數</string>
@@ -319,8 +312,6 @@
<string name="settings_eh_filter">屏蔽畫廊</string>
<string name="settings_eh_filter_summary">根據標題、上傳者、標籤屏蔽畫廊</string>
<string name="settings_read_volume_page">使用音量鍵翻頁</string>
<string name="settings_download">下載</string>
<string name="settings_download_download_location">下載路徑</string>
<string name="settings_download_cant_get_download_location">無法獲取下載路徑</string>
@@ -405,6 +396,7 @@
<string name="just_now">剛剛</string>
<string name="yesterday">昨天</string>
<string name="some_days_ago">%d 天前</string>
<string name="archive">壓縮包</string>
<string name="no_archives">沒有壓縮包</string>
<string name="download_archive_started">開始下載壓縮包</string>
<string name="download_archive_failure">無法下載壓縮包</string>
+4 -14
View File
@@ -114,9 +114,6 @@
<string name="no_comments">沒有留言</string>
<string name="no_more_comments">已顯示所有的留言了</string>
<string name="more_comment">更多留言</string>
<string name="no_previews">沒有預覽</string>
<string name="no_more_previews">已經顯示所有預覽了</string>
<string name="more_previews">更多預覽</string>
<string name="refresh">重新整理</string>
<string name="view_original">查看原圖</string>
<string name="open_in_other_app">在其他應用程式中開啟</string>
@@ -202,7 +199,6 @@
<string name="format_url">連結</string>
<string name="format_plain">純文字</string>
<!-- Gallery Previews -->
<string name="gallery_previews">圖庫預覽</string>
<string name="go_to">跳到</string>
<!-- Download -->
<string name="scene_download_title">正在下載 - %s</string>
@@ -298,9 +294,6 @@
<string name="settings_eh_detail_size_long">長</string>
<string name="settings_eh_detail_size_short">短</string>
<string name="settings_eh_thumb_columns">縮圖列數</string>
<string name="settings_eh_thumb_resolution">縮圖解析度</string>
<string name="settings_eh_thumb_resolution_summary">%s,指定解析度可能會使縮圖取得失敗</string>
<string name="settings_eh_thumb_resolution_auto">自動</string>
<string name="settings_eh_force_eh_thumb">使用 e-hentai 縮圖伺服器</string>
<string name="settings_eh_force_eh_thumb_summary">若縮圖載入遇到問題可嘗試停用此項</string>
<string name="settings_eh_show_jpn_title">顯示日文標題</string>
@@ -317,10 +310,6 @@
<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_read_volume_page">使用音量鍵翻頁</string>
<string name="settings_read_volume_page_fast">快</string>
<string name="settings_read_volume_page_slow">慢</string>
<string name="settings_read_reverse_volume">翻轉音量鍵</string>
<string name="settings_download">下載</string>
<string name="settings_download_download_location">下載路徑</string>
<string name="settings_download_cant_get_download_location">無法取得下載路徑</string>
@@ -410,12 +399,13 @@
<string name="just_now">剛剛</string>
<string name="yesterday">昨天</string>
<string name="some_days_ago">%d 天前</string>
<string name="no_archives">沒有壓縮檔</string>
<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="no_archives">沒有壓縮檔</string>
<string name="download_archive_started">開始下載壓縮檔</string>
<string name="download_archive_failure">下載失敗</string>
<string name="download_archive_failure_no_hath">下載壓縮檔需要 H@H 客戶端</string>
<string name="current_funds">目前資金:</string>
<string name="insufficient_funds">餘額不足</string>
-12
View File
@@ -74,18 +74,6 @@
<item>1</item>
</integer-array>
<string-array name="thumb_resolution_entries" translatable="false">
<item>@string/settings_eh_thumb_resolution_auto</item>
<item>250x</item>
<item>300x</item>
</string-array>
<integer-array name="thumb_resolution_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
<string-array name="search_min_rating" translatable="false">
<item>@string/any</item>
<item>@string/star_2</item>
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<item name="reader_pager" type="id" />
</resources>
+4 -16
View File
@@ -130,9 +130,6 @@
<string name="no_comments">No comments</string>
<string name="no_more_comments">No more comments</string>
<string name="more_comment">More comments</string>
<string name="no_previews">No previews</string>
<string name="no_more_previews">No more previews</string>
<string name="more_previews">More previews</string>
<string name="refresh">Refresh</string>
<string name="view_original">View original image</string>
<string name="open_in_other_app">Open with other app</string>
@@ -219,7 +216,6 @@
<string name="format_url">URL</string>
<string name="format_plain">Plain text</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>
@@ -314,9 +310,6 @@
<string name="settings_eh_detail_size_long">Long</string>
<string name="settings_eh_detail_size_short">Short</string>
<string name="settings_eh_thumb_columns">Thumb columns</string>
<string name="settings_eh_thumb_resolution">Thumb resolution</string>
<string name="settings_eh_thumb_resolution_summary">Currently %s. Certain resolutions might cause getting thumb failed</string>
<string name="settings_eh_thumb_resolution_auto">Auto</string>
<string name="settings_eh_force_eh_thumb">Use e-hentai thumbnail server</string>
<string name="settings_eh_force_eh_thumb_summary">Try disabling this if you have trouble loading thumbnails</string>
<string name="settings_eh_show_jpn_title">Show Japanese title</string>
@@ -333,10 +326,6 @@
<string name="settings_eh_tag_translations_source_url">https://placeholder</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_volume_page_fast">Fast</string>
<string name="settings_read_volume_page_slow">Slow</string>
<string name="settings_read_reverse_volume">Reverse volume key</string>
<string name="settings_block_extraneous_ads">[Experimental] Block extraneous ads</string>
<string name="settings_ads_placeholder">[Optional] Pick placeholder to replace ads</string>
<string name="settings_download">Download</string>
@@ -435,12 +424,13 @@
<string name="just_now">Just now</string>
<string name="yesterday">Yesterday</string>
<string name="some_days_ago">%d days ago</string>
<string name="no_archives">No Archives</string>
<string name="download_archive_started">Archive download started</string>
<string name="download_archive_failure">Failed to download archive</string>
<string name="archive">Archive</string>
<string name="archive_free">Free</string>
<string name="archive_original">Original</string>
<string name="archive_resample">Resample</string>
<string name="no_archives">No Archives</string>
<string name="download_archive_started">Archive download started</string>
<string name="download_archive_failure">Failed to download archive</string>
<string name="download_archive_failure_no_hath">Need H@H client for archive download</string>
<string name="current_funds">Current Funds:</string>
<string name="insufficient_funds">Insufficient Funds</string>
@@ -519,8 +509,6 @@
<string name="preload_thumb_aggressively">Preload thumbs aggressively</string>
<string name="animate_items">List item animations</string>
<string name="animate_items_summary">Try disabling this if you are facing crashes / frame drops</string>
<string name="desktop_site">Desktop site</string>
<string name="desktop_site_summary">Higher default image resolution</string>
<string name="auto_updates">Automatically check for updates</string>
<string name="update_frequency_never">Never</string>
<string name="update_frequency_daily">Daily</string>
+22 -22
View File
@@ -45,9 +45,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.91"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "autocfg"
@@ -108,9 +108,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cc"
version = "1.1.31"
version = "1.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf"
dependencies = [
"shlex",
]
@@ -190,9 +190,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "csv"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
dependencies = [
"csv-core",
"itoa",
@@ -332,9 +332,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]]
name = "iana-time-zone"
@@ -361,9 +361,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.4"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -436,9 +436,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.161"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "log"
@@ -774,9 +774,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
@@ -793,9 +793,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
@@ -816,9 +816,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "syn"
version = "2.0.85"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@@ -827,18 +827,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.65"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.65"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
+6 -4
View File
@@ -7,12 +7,14 @@ edition = "2021"
crate-type = ["cdylib"]
[features]
api-level-26 = ["ndk/api-level-26"]
jvm = ["dep:jni", "dep:jni_fn"]
android = ["dep:ndk", "jvm", "ndk/bitmap"]
android-26 = ["android", "ndk/api-level-26"]
[dependencies]
regex-lite = "0.1"
jni = "0.21"
jni_fn = "0.1"
jni = { version = "0.21", optional = true }
jni_fn = { version = "0.1", optional = true }
quick-xml = { default-features = false, version = "0.37" }
tl = "0.7"
log = "0.4"
@@ -22,7 +24,7 @@ serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
image = { default-features = false, version = "0.25" }
rxing = { default-features = false, version = "0.6" }
ndk = { version = "0.9", features = ["bitmap"] }
ndk = { version = "0.9", optional = true }
[profile.release]
panic = "abort"
+3
View File
@@ -0,0 +1,3 @@
mod android;
mod android_o;
mod jvm;
+55
View File
@@ -0,0 +1,55 @@
#![cfg(feature = "android")]
use super::jvm::jni_throwing;
use crate::img::border::detect_border;
use crate::img::qr_code::detect_image_ad;
use anyhow::{Context, Result};
use image::{ImageBuffer, Rgba};
use jni::objects::JClass;
use jni::sys::jboolean;
use jni::sys::{jintArray, jobject};
use jni::JNIEnv;
use jni_fn::jni_fn;
use ndk::bitmap::Bitmap;
use std::ptr::slice_from_raw_parts;
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
pub fn detectBorder(mut env: JNIEnv, _class: JClass, object: jobject) -> jintArray {
jni_throwing(&mut env, |env| {
let slice = with_bitmap_content(env, object, |img| Ok(detect_border(&img)))?;
let array = env.new_int_array(4)?;
env.set_int_array_region(&array, 0, &slice.context("Image too small!")?)?;
Ok(array.into_raw())
})
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
pub fn hasQrCode(mut env: JNIEnv, _class: JClass, object: jobject) -> jboolean {
jni_throwing(&mut env, |env| {
with_bitmap_content(env, object, |image| Ok(detect_image_ad(image) as jboolean))
})
}
pub fn with_bitmap_content<F, R>(env: &mut JNIEnv, bitmap: jobject, f: F) -> Result<R>
where
F: FnOnce(ImageBuffer<Rgba<u8>, &[u8]>) -> Result<R>,
{
// SAFETY: kotlin caller must ensure bitmap is valid.
let handle = unsafe { Bitmap::from_jni(env.get_raw(), bitmap) };
let info = handle.info()?;
let (width, height) = (info.width(), info.height());
let ptr = handle.lock_pixels()? as *const u8;
// SAFETY: maybe unsafe if bitmap buffer not RGBA8888 format.
let buffer = unsafe { &*slice_from_raw_parts(ptr, (width * height * 4) as usize) };
let image = ImageBuffer::from_raw(width, height, buffer);
let result = image.context("Image buffer not RGBA8888!!!").and_then(f);
handle.unlock_pixels()?;
result
}
@@ -1,6 +1,7 @@
#![cfg(feature = "api-level-26")]
#![cfg(feature = "android-26")]
use crate::{jni_throwing, with_bitmap_content};
use super::android::with_bitmap_content;
use super::jvm::jni_throwing;
use anyhow::Context;
use image::{GenericImage, GenericImageView, ImageBuffer};
use jni::objects::JClass;
+143
View File
@@ -0,0 +1,143 @@
#![cfg(feature = "jvm")]
use crate::parser::config::parse_fav_cat;
use crate::parser::fav::parse_fav;
use crate::parser::home::parse_limit;
use crate::parser::list::parse_info_list;
use crate::parser::torrent::parse_torrent_list;
use crate::EhError;
use android_logger::Config;
use anyhow::{ensure, Context, Result};
use jni::objects::{JByteBuffer, JClass};
use jni::sys::{jboolean, jint, jobject, JNI_VERSION_1_6};
use jni::{JNIEnv, JavaVM};
use jni_fn::jni_fn;
use log::LevelFilter;
use serde::Serialize;
use std::ffi::c_void;
use std::io::Cursor;
use std::ptr::slice_from_raw_parts_mut;
use std::str::from_utf8_unchecked;
use tl::{ParserOptions, VDom};
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.FavoritesParserKt")]
pub fn parseFav(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, html| {
parse_fav(dom, dom.parser(), html)
})
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.HomeParserKt")]
pub fn parseLimit(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, _| {
parse_limit(dom, dom.parser()).context("Can't parse Limit")
})
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.GalleryListParserKt")]
pub fn parseGalleryInfoList(mut env: JNIEnv, _: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, str| {
parse_info_list(dom, dom.parser(), str)
})
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.TorrentParserKt")]
pub fn parseTorrent(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, _| {
parse_torrent_list(dom, dom.parser())
})
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.UserConfigParser")]
pub fn parseFavCat(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |_, body| Ok(parse_fav_cat(body)))
}
fn deref_mut_direct_bytebuffer<'local>(
env: &JNIEnv,
buffer: JByteBuffer<'local>,
) -> Result<&'local mut [u8]> {
let ptr = env.get_direct_buffer_address(&buffer)?;
let cap = env.get_direct_buffer_capacity(&buffer)?;
// SAFETY: jni contract, buffer must alive through native call.
Ok(unsafe { &mut *slice_from_raw_parts_mut(ptr, cap) })
}
pub trait ThrowingHasDefault {
fn default() -> Self;
}
impl ThrowingHasDefault for jboolean {
fn default() -> Self {
0 as jboolean
}
}
impl ThrowingHasDefault for jobject {
fn default() -> Self {
0 as jobject
}
}
impl ThrowingHasDefault for i32 {
fn default() -> Self {
0
}
}
impl ThrowingHasDefault for () {
fn default() -> Self {}
}
pub fn jni_throwing<F, R>(env: &mut JNIEnv, f: F) -> R
where
F: FnOnce(&mut JNIEnv) -> Result<R>,
R: ThrowingHasDefault,
{
match f(env) {
Ok(value) => value,
Err(err) => {
let msg = format!("{err}");
env.throw_new("java/lang/RuntimeException", msg).ok();
R::default()
}
}
}
pub fn parse_marshal_inplace<F, R>(env: &mut JNIEnv, str: JByteBuffer, limit: jint, f: F) -> i32
where
F: Fn(&VDom, &str) -> Result<R>,
R: Serialize,
{
jni_throwing(env, |env| {
let buffer = deref_mut_direct_bytebuffer(env, str)?;
let value = {
// SAFETY: ktor client ensure html content is valid utf-8.
let html = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) };
let dom = tl::parse(html, ParserOptions::default())?;
ensure!(dom.version().is_some(), EhError::Error(html.to_string()));
f(&dom, html)?
};
let mut cursor = Cursor::new(buffer);
serde_cbor::to_writer(&mut cursor, &value)?;
Ok(cursor.position() as i32)
})
}
#[no_mangle]
pub extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint {
android_logger::init_once(Config::default().with_max_level(LevelFilter::Debug));
JNI_VERSION_1_6
}
+2 -3
View File
@@ -1,3 +1,2 @@
mod border;
mod copy_ahb;
mod qr_code;
pub mod border;
pub mod qr_code;
+2 -19
View File
@@ -1,10 +1,4 @@
use crate::{jni_throwing, with_bitmap_content};
use anyhow::Context;
use image::{ImageBuffer, Luma, Pixel, Rgba};
use jni::objects::JClass;
use jni::sys::{jintArray, jobject};
use jni::JNIEnv;
use jni_fn::jni_fn;
/** A line will be considered as having content if 0.25% of it is filled. */
static FILLED_RATIO_LIMIT: f32 = 0.0025;
@@ -72,7 +66,8 @@ fn detect_border_lines<'pixel>(
}
}
fn detect_border(image: &ImageBuffer<Rgba<u8>, &[u8]>) -> Option<[i32; 4]> {
#[allow(dead_code)]
pub fn detect_border(image: &ImageBuffer<Rgba<u8>, &[u8]>) -> Option<[i32; 4]> {
let (w, h) = image.dimensions();
let top = detect_border_lines(image.rows(), w);
let bottom = detect_border_lines(image.rows().rev(), w);
@@ -81,15 +76,3 @@ fn detect_border(image: &ImageBuffer<Rgba<u8>, &[u8]>) -> Option<[i32; 4]> {
let right = detect_border_lines(iter.rev(), h);
Some([left, top, w as i32 - right, h as i32 - bottom])
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
pub fn detectBorder(mut env: JNIEnv, _class: JClass, object: jobject) -> jintArray {
jni_throwing(&mut env, |env| {
let slice = with_bitmap_content(env, object, |img| Ok(detect_border(&img)))?;
let array = env.new_int_array(4)?;
env.set_int_array_region(&array, 0, &slice.context("Image too small!")?)?;
Ok(array.into_raw())
})
}
+2 -15
View File
@@ -1,16 +1,12 @@
use crate::{jni_throwing, with_bitmap_content};
use image::{ImageBuffer, Rgba};
use jni::objects::JClass;
use jni::sys::{jboolean, jobject};
use jni::JNIEnv;
use jni_fn::jni_fn;
use rxing::common::HybridBinarizer;
use rxing::qrcode::detector::FinderPatternFinder;
use rxing::{BinaryBitmap, DecodingHintDictionary, Point, RGBLuminanceSource};
use std::f32::consts::FRAC_1_SQRT_2;
use std::ptr::slice_from_raw_parts;
fn detect_image_ad(image: ImageBuffer<Rgba<u8>, &[u8]>) -> bool {
#[allow(dead_code)]
pub fn detect_image_ad(image: ImageBuffer<Rgba<u8>, &[u8]>) -> bool {
let (width, height) = (image.width() as usize, image.height() as usize);
let buffer = image.into_raw();
let pixels = unsafe { &*slice_from_raw_parts(buffer.as_ptr() as *const u32, buffer.len()) };
@@ -30,12 +26,3 @@ fn detect_image_ad(image: ImageBuffer<Rgba<u8>, &[u8]>) -> bool {
Err(_) => false,
}
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
pub fn hasQrCode(mut env: JNIEnv, _class: JClass, object: jobject) -> jboolean {
jni_throwing(&mut env, |env| {
with_bitmap_content(env, object, |image| Ok(detect_image_ad(image) as jboolean))
})
}
+1 -113
View File
@@ -1,21 +1,8 @@
mod ffi;
mod img;
mod parser;
use android_logger::Config;
use anyhow::{ensure, Context, Result};
use image::{ImageBuffer, Rgba};
use jni::objects::JByteBuffer;
use jni::sys::{jboolean, jint, jobject, JavaVM, JNI_VERSION_1_6};
use jni::JNIEnv;
use log::LevelFilter;
use ndk::bitmap::Bitmap;
use serde::Serialize;
use std::ffi::c_void;
use std::fmt::{Debug, Display, Formatter};
use std::io::Cursor;
use std::ptr::{slice_from_raw_parts, slice_from_raw_parts_mut};
use std::str::from_utf8_unchecked;
use tl::ParserOptions;
use tl::{Bytes, Node, NodeHandle, Parser, VDom};
#[macro_export]
@@ -58,43 +45,6 @@ where
handle.get(parser)
}
fn deref_mut_direct_bytebuffer<'local>(
env: &JNIEnv,
buffer: JByteBuffer<'local>,
) -> Result<&'local mut [u8]> {
let ptr = env.get_direct_buffer_address(&buffer)?;
let cap = env.get_direct_buffer_capacity(&buffer)?;
// SAFETY: jni contract, buffer must alive through native call.
Ok(unsafe { &mut *slice_from_raw_parts_mut(ptr, cap) })
}
trait ThrowingHasDefault {
fn default() -> Self;
}
impl ThrowingHasDefault for jboolean {
fn default() -> Self {
0 as jboolean
}
}
impl ThrowingHasDefault for jobject {
fn default() -> Self {
0 as jobject
}
}
impl ThrowingHasDefault for i32 {
fn default() -> Self {
0
}
}
impl ThrowingHasDefault for () {
fn default() -> Self {}
}
#[derive(Debug)]
enum EhError {
NoHits,
@@ -115,42 +65,6 @@ impl Display for EhError {
}
}
fn jni_throwing<F, R>(env: &mut JNIEnv, f: F) -> R
where
F: FnOnce(&mut JNIEnv) -> Result<R>,
R: ThrowingHasDefault,
{
match f(env) {
Ok(value) => value,
Err(err) => {
let msg = format!("{err}");
env.throw_new("java/lang/RuntimeException", msg).ok();
R::default()
}
}
}
fn parse_marshal_inplace<F, R>(env: &mut JNIEnv, str: JByteBuffer, limit: jint, f: F) -> i32
where
F: Fn(&VDom, &str) -> Result<R>,
R: Serialize,
{
jni_throwing(env, |env| {
let buffer = deref_mut_direct_bytebuffer(env, str)?;
let value = {
// SAFETY: ktor client ensure html content is valid utf-8.
let html = unsafe { from_utf8_unchecked(&buffer[..limit as usize]) };
let dom = tl::parse(html, ParserOptions::default())?;
ensure!(dom.version().is_some(), EhError::Error(html.to_string()));
f(&dom, html)?
};
let mut cursor = Cursor::new(buffer);
serde_cbor::to_writer(&mut cursor, &value)?;
Ok(cursor.position() as i32)
})
}
fn get_node_handle_attr<'a>(
node: &NodeHandle,
parser: &'a Parser,
@@ -173,29 +87,3 @@ fn query_childs_first_match_attr<'a>(
let mut iter = node.as_tag()?.query_selector(parser, &selector)?;
get_node_handle_attr(&iter.next()?, parser, attr)
}
fn with_bitmap_content<F, R>(env: &mut JNIEnv, bitmap: jobject, f: F) -> Result<R>
where
F: FnOnce(ImageBuffer<Rgba<u8>, &[u8]>) -> Result<R>,
{
// SAFETY: kotlin caller must ensure bitmap is valid.
let handle = unsafe { Bitmap::from_jni(env.get_raw(), bitmap) };
let info = handle.info()?;
let (width, height) = (info.width(), info.height());
let ptr = handle.lock_pixels()? as *const u8;
// SAFETY: maybe unsafe if bitmap buffer not RGBA8888 format.
let buffer = unsafe { &*slice_from_raw_parts(ptr, (width * height * 4) as usize) };
let image = ImageBuffer::from_raw(width, height, buffer);
let result = image.context("Image buffer not RGBA8888!!!").and_then(f);
handle.unlock_pixels()?;
result
}
#[no_mangle]
pub extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint {
android_logger::init_once(Config::default().with_max_level(LevelFilter::Debug));
JNI_VERSION_1_6
}
+5 -5
View File
@@ -1,5 +1,5 @@
mod config;
mod fav;
mod home;
mod list;
mod torrent;
pub mod config;
pub mod fav;
pub mod home;
pub mod list;
pub mod torrent;
+7 -16
View File
@@ -1,19 +1,10 @@
use crate::{parse_marshal_inplace, regex};
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use crate::regex;
use quick_xml::escape::unescape;
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.UserConfigParser")]
pub fn parseFavCat(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |_, body| {
let cat = regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"")
.captures_iter(body)
.filter_map(|c| unescape(&c[1]).map(|s| s.to_string()).ok())
.collect::<Vec<_>>();
Ok(cat)
})
#[allow(dead_code)]
pub fn parse_fav_cat(html: &str) -> Vec<String> {
regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"")
.captures_iter(html)
.filter_map(|c| unescape(&c[1]).map(|s| s.to_string()).ok())
.collect()
}
+4 -16
View File
@@ -1,10 +1,6 @@
use crate::parser::list::{parse_info_list, GalleryListResult};
use crate::{parse_marshal_inplace, EhError};
use crate::EhError;
use anyhow::{bail, Result};
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use quick_xml::escape::unescape;
use serde::Serialize;
use tl::Parser;
@@ -12,13 +8,14 @@ use tl::VDom;
#[derive(Serialize)]
#[allow(non_snake_case)]
struct FavResult {
pub struct FavResult {
catArray: Vec<String>,
countArray: Vec<i32>,
galleryListResult: GalleryListResult,
}
fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Result<FavResult> {
#[allow(dead_code)]
pub fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Result<FavResult> {
if html.contains("This page requires you to log on.</p>") {
bail!(EhError::NeedLogin)
}
@@ -53,12 +50,3 @@ fn parse_fav(dom: &VDom, parser: &Parser, html: &str) -> Result<FavResult> {
bail!("Illegal fav cat count!")
}
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.FavoritesParserKt")]
pub fn parseFav(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, html| {
parse_fav(dom, dom.parser(), html)
})
}
+4 -17
View File
@@ -1,22 +1,18 @@
use crate::{get_vdom_first_element_by_class_name, parse_marshal_inplace};
use anyhow::Context;
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use crate::get_vdom_first_element_by_class_name;
use serde::Serialize;
use tl::Parser;
use tl::VDom;
#[allow(non_snake_case)]
#[derive(Serialize)]
struct Limits {
pub struct Limits {
current: i32,
maximum: i32,
resetCost: i32,
}
fn parse_limit(dom: &VDom, parser: &Parser) -> Option<Limits> {
#[allow(dead_code)]
pub fn parse_limit(dom: &VDom, parser: &Parser) -> Option<Limits> {
let limit_box = get_vdom_first_element_by_class_name(dom, "homebox")?.as_tag()?;
let vec = limit_box
.query_selector(parser, "strong")?
@@ -46,12 +42,3 @@ fn parse_limit(dom: &VDom, parser: &Parser) -> Option<Limits> {
_ => None,
}
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.HomeParserKt")]
pub fn parseLimit(mut env: JNIEnv, _class: JClass, input: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, input, limit, |dom, _| {
parse_limit(dom, dom.parser()).context("Can't parse Limit")
})
}
+2 -14
View File
@@ -1,13 +1,9 @@
use crate::EhError;
use crate::{get_element_by_id, get_vdom_first_element_by_class_name};
use crate::{get_first_element_by_class_name, query_childs_first_match_attr};
use crate::{get_node_attr, get_node_handle_attr, regex};
use crate::{parse_marshal_inplace, EhError};
use crate::{EHGT_PREFIX, EX_PREFIX};
use anyhow::{bail, Context, Result};
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use quick_xml::escape::unescape;
use serde::Serialize;
use std::borrow::Cow;
@@ -197,6 +193,7 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option<BaseGalleryInfo> {
})
}
#[allow(dead_code)]
pub fn parse_info_list(dom: &VDom, parser: &Parser, str: &str) -> Result<GalleryListResult> {
if str.contains("<p>You do not have any watched tags") {
bail!(EhError::NoWatched)
@@ -241,12 +238,3 @@ pub fn parse_info_list(dom: &VDom, parser: &Parser, str: &str) -> Result<Gallery
};
f().context("No content")
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.GalleryListParserKt")]
pub fn parseGalleryInfoList(mut env: JNIEnv, _: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, str| {
parse_info_list(dom, dom.parser(), str)
})
}
+4 -16
View File
@@ -1,15 +1,11 @@
use crate::{parse_marshal_inplace, regex};
use crate::regex;
use anyhow::{Context, Result};
use jni::objects::{JByteBuffer, JClass};
use jni::sys::jint;
use jni::JNIEnv;
use jni_fn::jni_fn;
use quick_xml::escape::unescape;
use serde::Serialize;
use tl::{Parser, VDom};
#[derive(Serialize)]
struct Torrent {
pub struct Torrent {
outdated: bool,
posted: String,
size: String,
@@ -21,7 +17,8 @@ struct Torrent {
name: String,
}
fn parse_torrent_list(dom: &VDom, parser: &Parser) -> Result<Vec<Torrent>> {
#[allow(dead_code)]
pub fn parse_torrent_list(dom: &VDom, parser: &Parser) -> Result<Vec<Torrent>> {
let list = dom.query_selector("table").context("No Table")?.filter_map(|e| {
let html = e.get(parser)?.inner_html(parser);
if html.contains("Expunged") {
@@ -44,12 +41,3 @@ fn parse_torrent_list(dom: &VDom, parser: &Parser) -> Result<Vec<Torrent>> {
}).collect::<Vec<Torrent>>();
Ok(list)
}
#[no_mangle]
#[allow(non_snake_case)]
#[jni_fn("com.hippo.ehviewer.client.parser.TorrentParserKt")]
pub fn parseTorrent(mut env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jint) -> jint {
parse_marshal_inplace(&mut env, buffer, limit, |dom, _| {
parse_torrent_list(dom, dom.parser())
})
}
+112
View File
@@ -0,0 +1,112 @@
<p align="right">
<a href="/README.md">
English
</a>
<span> | </span>
<a href="/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="/docs/README/zh-tw.md">
正體中文
</a>
<span> | </span>
<strong>日本語</strong>
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="#説明">
説明
</a>
<span> | </span>
<a href="#ダウンロード">
ダウンロード
</a>
<span> | </span>
<a href="#スクリーンショット">
スクリーンショット
</a>
<span> | </span>
<a href="#感謝">
感謝
</a>
<span> | </span>
<a href="#ライセンス">
ライセンス
</a>
</h3>
</div>
# 説明
軽量で高性能な EhViewer のフォークです。
[Material Design 3](https://m3.material.io/) と[ダイナミックカラー](https://m3.material.io/styles/color/dynamic-color/overview)のサポートを搭載しています。
# ダウンロード
| フレーバー | 最小の Android バージョン | 注意事項 |
|-------------|-------------------------|-------------------------------------------|
| Default | 8.0 | アニメーション WebP のサポートは Android 9 以降が必要です |
| Marshmallow | 6.0 | サポートは限定的、保証はできません |
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
# スクリーンショット
![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)
# 感謝
使用しているライブラリ
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](https://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [Material Icons](https://github.com/google/material-design-icons)
- [Ktor](https://ktor.io/)
- [Coil](https://coil-kt.github.io/coil/)
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
- [libarchive](https://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
# ライセンス
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
Copyright 2023-2024 Foolbar
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with EhViewer. If not, see <https://www.gnu.org/licenses/>.
+11 -5
View File
@@ -8,6 +8,10 @@
<a href="/docs/README/zh-tw.md">
正體中文
</a>
<span> | </span>
<a href="/docs/README/ja.md">
日本語
</a>
</p>
<h1 align="center">
@@ -59,16 +63,18 @@
# 描述
致力于轻量化与良好性能表现的 EhViewer 分支
致力于高性能的现代 EhViewer 分支
已针对性适配 Material Design 3 及其动态取色系统
# 下载
| 变种 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支持 |
| Marshmallow | Android 6.0-7.1, 有限支持 |
| 变种 | 最低 Android 版本 | 备注 |
|-------------|---------------|-------------------------|
| Default | 8.0 | 动画 WebP 支持需要 Android 9+ |
| Marshmallow | 6.0* | 有限支持,无保证 |
*运行 Android 6 的设备需安装 [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) 证书
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
+8 -4
View File
@@ -8,6 +8,10 @@
</a>
<span> | </span>
<strong>正體中文</strong>
<span> | </span>
<a href="/docs/README/ja.md">
日本語
</a>
</p>
<h1 align="center">
@@ -65,10 +69,10 @@
# 下載
| 變種 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支援 |
| Marshmallow | Android 6.0-7.1, 有限支援 |
| 變種 | 最低 Android 版本 | 備註 |
|-------------|---------------|-------------------------|
| Default | 8.0 | 動畫 WebP 支援需要 Android 9+ |
| Marshmallow | 6.0 | 有限支援,無保證 |
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
+5 -5
View File
@@ -16,20 +16,20 @@ androidx-paging = "3.3.2"
androidx-room = "2.7.0-alpha11"
androidx-work = "2.10.0"
arrow = "1.2.4"
coil = "3.0.0-rc02"
coil = "3.0.2"
compose-bom = "2024.10.01"
compose-destinations = "2.1.0-beta14"
kotlin = "2.1.0-Beta2"
kotlin = "2.1.0-RC"
kotlinx-coroutines = "1.9.0"
kotlinx-datetime = "0.6.1"
kotlinx-serialization = "1.7.3"
ksp = "2.1.0-Beta2-1.0.26"
ksp = "2.1.0-RC-1.0.27"
ktor = "3.0.1"
okhttp = "5.0.0-alpha.14"
okio = "3.9.1"
splitties = "3.0.0"
telephoto = "0.13.0"
xmlutil = "0.90.2"
xmlutil = "0.90.3"
junit = "1.2.1"
uiautomator = "2.4.0-alpha01"
benchmark = "1.4.0-alpha04"
@@ -83,7 +83,7 @@ compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinat
material-motion-core = "io.github.fornewid:material-motion-compose-core:2.0.1"
material-kolor = "com.materialkolor:material-kolor:2.0.0"
desugar = "com.android.tools:desugar_jdk_libs:2.1.2"
desugar = "com.android.tools:desugar_jdk_libs:2.1.3"
diff = "io.github.petertrr:kotlin-multiplatform-diff:0.7.0"
jsoup = "org.jsoup:jsoup:1.18.1"