Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571969fca9 | ||
|
|
16fa52790c | ||
|
|
c88c252587 | ||
|
|
253cb03de8 | ||
|
|
0b74adc13b | ||
|
|
6fc2328475 | ||
|
|
e9c4201acc | ||
|
|
9edb52c909 | ||
|
|
f4c7320bb5 | ||
|
|
17581ca204 | ||
|
|
11edb8ce5d | ||
|
|
40caa7b1eb | ||
|
|
e3f4c24983 | ||
|
|
dd0482cb26 | ||
|
|
4052007de8 | ||
|
|
85b03d9d8a | ||
|
|
36c3fd6f5d | ||
|
|
d397a5d35e | ||
|
|
43d83ad919 | ||
|
|
e71dd1d576 | ||
|
|
72e28b8e04 | ||
|
|
3432a15c62 | ||
|
|
45a5bae8b2 | ||
|
|
a566393a89 | ||
|
|
5f2f3f9122 | ||
|
|
c7fccf7e70 | ||
|
|
3d92d061f1 | ||
|
|
f2759675a1 | ||
|
|
2e6ff78662 | ||
|
|
8ca0a32932 | ||
|
|
72fbcd4e0e | ||
|
|
a5719a9c1d | ||
|
|
bad41d5027 | ||
|
|
3bfb95f190 | ||
|
|
f04695feb6 | ||
|
|
94da911f93 | ||
|
|
f4771d4ddb | ||
|
|
9f7c4adb0f | ||
|
|
6fb20fc6c1 | ||
|
|
ce8e91aab8 |
No files matched your search
@@ -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
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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\"")
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
+1
-1
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">下記の情報は誰でもこのアカウントにログインできますので<br><b>絶対に他人に渡さないように!</b></string>
|
||||
<string name="settings_eh_identity_cookies_signed">このアカウントは Identity Cookie を使用してログインできます。<br><b>これは安全に保管してください</b></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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Generated
+22
-22
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod android;
|
||||
mod android_o;
|
||||
mod jvm;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
mod border;
|
||||
mod copy_ahb;
|
||||
mod qr_code;
|
||||
pub mod border;
|
||||
pub mod qr_code;
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
# スクリーンショット
|
||||
|
||||

|
||||

|
||||
|
||||
# 感謝
|
||||
|
||||
使用しているライブラリ
|
||||
|
||||
- [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
@@ -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,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"/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user