Compare commits

...

7 Commits

Author SHA1 Message Date
꧁[C̲̅j̲̅s̲̅a̲̅h̲̅]꧂
9a1c1db427
Merge 94aaee8d9b into 7422db5fde 2024-06-28 02:45:26 -07:00
Horis
7422db5fde 优化 2024-06-28 12:34:00 +08:00
Horis
77238c16e2 优化 2024-06-28 12:20:18 +08:00
Horis
c2b0c200fb 优化 2024-06-27 20:33:10 +08:00
Cjsah
94aaee8d9b
resolve refresh 2024-06-16 10:51:12 +08:00
Cjsah
96a3b6b802
remove .dev 2024-06-16 10:39:37 +08:00
Cjsah
f3d055c9bb
update set ip 2024-06-16 10:27:43 +08:00
17 changed files with 249 additions and 36 deletions

View File

@ -11,7 +11,7 @@ body:
required: true required: true
- label: 最新[测试版](https://github.com/gedoor/legado/actions/workflows/test.yml)依然存在此问题 / Latest beta app does not work - label: 最新[测试版](https://github.com/gedoor/legado/actions/workflows/test.yml)依然存在此问题 / Latest beta app does not work
required: true required: true
- label: 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件等无关 / Make sure your machine is not touched by hook frameworks, plugins etc - label: 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件、无障碍服务等无关 / Make sure your machine is not touched by hook frameworks, plugins, accessibility etc
required: true required: true
- type: textarea - type: textarea

View File

@ -36,7 +36,9 @@ import io.legado.app.utils.ChineseUtils
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.isDebuggable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.chromium.base.ThreadUtils
import splitties.init.appCtx import splitties.init.appCtx
import splitties.systemservices.notificationManager import splitties.systemservices.notificationManager
import java.net.URL import java.net.URL
@ -51,6 +53,9 @@ class App : Application() {
super.onCreate() super.onCreate()
LogUtils.d("App", "onCreate") LogUtils.d("App", "onCreate")
LogUtils.logDeviceInfo() LogUtils.logDeviceInfo()
if (isDebuggable) {
ThreadUtils.setThreadAssertsDisabledForTesting(true)
}
oldConfig = Configuration(resources.configuration) oldConfig = Configuration(resources.configuration)
CrashHandler(this) CrashHandler(this)
//预下载Cronet so //预下载Cronet so

View File

@ -10,6 +10,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.encodeURI
import io.legado.app.utils.isXml import io.legado.app.utils.isXml
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
import me.ag2s.epublib.domain.EpubBook import me.ag2s.epublib.domain.EpubBook
@ -252,8 +253,10 @@ class EpubFile(var book: Book) {
} }
} }
bodyElement.select("img").forEach { bodyElement.select("img").forEach {
val src = it.attr("src") val src = it.attr("src").encodeURI()
it.attr("src", URI(res.href).resolve(src).toString()) val href = res.href.encodeURI()
val resolvedHref = URLDecoder.decode(URI(href).resolve(src).toString(), "UTF-8")
it.attr("src", resolvedHref)
} }
return bodyElement return bodyElement
} }

View File

@ -17,6 +17,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
@ -384,3 +385,6 @@ val Context.channel: String
} }
return "" return ""
} }
val Context.isDebuggable: Boolean
get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0

View File

@ -7,12 +7,13 @@ import android.icu.text.Collator
import android.icu.util.ULocale import android.icu.util.ULocale
import android.net.Uri import android.net.Uri
import android.text.Editable import android.text.Editable
import cn.hutool.core.net.URLEncodeUtil
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.AppPattern.dataUriRegex import io.legado.app.constant.AppPattern.dataUriRegex
import java.io.File import java.io.File
import java.lang.Character.codePointCount import java.lang.Character.codePointCount
import java.lang.Character.offsetByCodePoints import java.lang.Character.offsetByCodePoints
import java.util.* import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
@ -132,3 +133,4 @@ fun String.escapeRegex(): String {
return replace(AppPattern.regexCharRegex, "\\\\$0") return replace(AppPattern.regexCharRegex, "\\\\$0")
} }
fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this)

View File

@ -35,6 +35,7 @@ import me.ag2s.epublib.domain.Spine;
import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.domain.SpineReference;
import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
import me.ag2s.epublib.util.URLEncodeUtil;
/** /**
* Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf
@ -108,6 +109,7 @@ public class PackageDocumentReader extends PackageDocumentBase {
for (int i = 0; i < originItemElements.getLength(); i++) { for (int i = 0; i < originItemElements.getLength(); i++) {
Element itemElement = (Element) originItemElements.item(i).cloneNode(false); Element itemElement = (Element) originItemElements.item(i).cloneNode(false);
String href = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href); String href = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href);
href = URLEncodeUtil.encode(href);
String resolvedHref = packagePath.resolve(href).toString(); String resolvedHref = packagePath.resolve(href).toString();
itemElement.setAttribute("href", resolvedHref); itemElement.setAttribute("href", resolvedHref);
fixedElements.add(itemElement); fixedElements.add(itemElement);
@ -451,9 +453,9 @@ public class PackageDocumentReader extends PackageDocumentBase {
OPFTags.item, OPFAttributes.id, coverResourceId, OPFTags.item, OPFAttributes.id, coverResourceId,
OPFAttributes.href); OPFAttributes.href);
if (StringUtil.isNotBlank(coverHref)) { if (StringUtil.isNotBlank(coverHref)) {
result.add(packagePath.resolve(coverHref).toString()); result.add(resolvePath(packagePath, coverHref));
} else { } else {
String resolved = packagePath.resolve(coverResourceId).toString(); String resolved = resolvePath(packagePath, coverResourceId);
result.add( result.add(
resolved); // maybe there was a cover href put in the cover id attribute resolved); // maybe there was a cover href put in the cover id attribute
} }
@ -464,11 +466,21 @@ public class PackageDocumentReader extends PackageDocumentBase {
OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover, OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover,
OPFAttributes.href); OPFAttributes.href);
if (StringUtil.isNotBlank(coverHref)) { if (StringUtil.isNotBlank(coverHref)) {
result.add(packagePath.resolve(coverHref).toString()); result.add(resolvePath(packagePath, coverHref));
} }
return result; return result;
} }
private static String resolvePath(URI parentPath, String href) {
href = URLEncodeUtil.encode(href);
String resolved = parentPath.resolve(href).toString();
try {
return URLDecoder.decode(resolved, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/** /**
* Finds the cover resource in the packageDocument and adds it to the book if found. * Finds the cover resource in the packageDocument and adds it to the book if found.
* Keeps the cover resource in the resources map * Keeps the cover resource in the resources map

View File

@ -0,0 +1,118 @@
package me.ag2s.epublib.util;
import java.io.CharArrayWriter;
import java.nio.charset.Charset;
import java.util.BitSet;
import java.util.Objects;
import kotlin.text.Charsets;
public class URLEncodeUtil {
static BitSet dontNeedEncoding;
static final int caseDiff = ('a' - 'A');
static {
dontNeedEncoding = new BitSet(256);
int i;
for (i = 'a'; i <= 'z'; i++) {
dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
dontNeedEncoding.set(i);
}
String mark = "-_.!~*'()";
for (i = 0; i < mark.length(); i++) {
dontNeedEncoding.set(mark.charAt(i));
}
String reversed = ";/?:@&=+$,#";
for (i = 0; i < reversed.length(); i++) {
dontNeedEncoding.set(reversed.charAt(i));
}
}
public static String encode(String s, Charset charset) {
Objects.requireNonNull(charset, "charset");
boolean needToChange = false;
StringBuilder out = new StringBuilder(s.length());
CharArrayWriter charArrayWriter = new CharArrayWriter();
for (int i = 0; i < s.length(); ) {
int c = s.charAt(i);
//System.out.println("Examining character: " + c);
if (dontNeedEncoding.get(c)) {
//System.out.println("Storing: " + c);
out.append((char) c);
i++;
} else {
// convert to external encoding before hex conversion
do {
charArrayWriter.write(c);
/*
* If this character represents the start of a Unicode
* surrogate pair, then pass in two characters. It's not
* clear what should be done if a byte reserved in the
* surrogate pairs range occurs outside of a legal
* surrogate pair. For now, just treat it as if it were
* any other character.
*/
if (c >= 0xD800 && c <= 0xDBFF) {
/*
System.out.println(Integer.toHexString(c)
+ " is high surrogate");
*/
if ((i + 1) < s.length()) {
int d = (int) s.charAt(i + 1);
/*
System.out.println("\tExamining "
+ Integer.toHexString(d));
*/
if (d >= 0xDC00 && d <= 0xDFFF) {
/*
System.out.println("\t"
+ Integer.toHexString(d)
+ " is low surrogate");
*/
charArrayWriter.write(d);
i++;
}
}
}
i++;
} while (i < s.length() && !dontNeedEncoding.get((c = (int) s.charAt(i))));
charArrayWriter.flush();
String str = new String(charArrayWriter.toCharArray());
byte[] ba = str.getBytes(charset);
for (int j = 0; j < ba.length; j++) {
out.append('%');
char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
// converting to use uppercase letter as part of
// the hex value if ch is a letter.
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
ch = Character.forDigit(ba[j] & 0xF, 16);
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
}
charArrayWriter.reset();
needToChange = true;
}
}
return (needToChange ? out.toString() : s);
}
public static String encode(String s) {
return encode(s, Charsets.UTF_8);
}
}

View File

@ -1 +0,0 @@
VITE_API=http://192.168.10.11:1122

View File

@ -1 +0,0 @@
VITE_API=

View File

@ -12,9 +12,8 @@
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 | | Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
## 开发 ## 开发
> 需要阅读app提供后端服务,开发前修改环境变量`VITE_API`为阅读web服务地址 > 需要阅读app提供后端服务
```bash ```bash
echo "VITE_API=http://<ip>:<port>" > .env.development
pnpm dev pnpm dev
``` ```

View File

@ -1,10 +1,25 @@
import axios from "axios"; import axios from "axios";
const SECOND = 1000; const SECOND = 1000;
const remoteIp = ref(localStorage.getItem("remoteIp"));
const ajax = axios.create({ const ajax = axios.create({
baseURL: import.meta.env.VITE_API || location.origin, // baseURL: import.meta.env.VITE_API || location.origin,
timeout: 120 * SECOND, timeout: 120 * SECOND,
}); });
ajax.interceptors.request.use((config) => {
config.baseURL = remoteIp.value;
return config;
});
export default ajax; export default ajax;
export const setRemoteIp = (ip) => {
remoteIp.value = ip;
localStorage.setItem("remoteIp", ip);
};
export const baseUrl = () => {
return remoteIp.value;
};

View File

@ -1,10 +1,13 @@
import ajax from "./axios"; import ajax, { baseUrl } from "./axios";
import { ElMessage } from "element-plus/es"; import { ElMessage } from "element-plus/es";
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/api */ /** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/api */
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */ /** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */
const { hostname, port } = new URL(import.meta.env.VITE_API || location.origin); const getUrl = () => {
const { hostname, port } = new URL(baseUrl());
return `${hostname}:${Number(port) + 1}`;
};
const isSourecEditor = /source/i.test(location.href); const isSourecEditor = /source/i.test(location.href);
const APIExceptionHandler = (error) => { const APIExceptionHandler = (error) => {
@ -29,7 +32,7 @@ const saveBookProgressWithBeacon = (bookProgress) => {
if (!bookProgress) return; if (!bookProgress) return;
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon // 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
navigator.sendBeacon( navigator.sendBeacon(
`${import.meta.env.VITE_API || location.origin}/saveBookProgress`, `${baseUrl()}/saveBookProgress`,
JSON.stringify(bookProgress), JSON.stringify(bookProgress),
); );
}; };
@ -56,7 +59,7 @@ const search = (
/** @type {() => void} */ onFinish, /** @type {() => void} */ onFinish,
) => { ) => {
// webSocket // webSocket
const url = `ws://${hostname}:${Number(port) + 1}/searchBook`; const url = `ws://${getUrl()}/searchBook`;
const socket = new WebSocket(url); const socket = new WebSocket(url);
socket.onopen = () => { socket.onopen = () => {
@ -100,7 +103,7 @@ const debug = (
/** @type {() => void} */ onFinish, /** @type {() => void} */ onFinish,
) => { ) => {
// webSocket // webSocket
const url = `ws://${hostname}:${Number(port) + 1}/${ const url = `ws://${getUrl()}/${
isBookSource ? "bookSource" : "rssSource" isBookSource ? "bookSource" : "rssSource"
}Debug`; }Debug`;

View File

@ -82,5 +82,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
} }

View File

@ -50,15 +50,14 @@
</template> </template>
<script setup> <script setup>
import { dateFormat } from "../utils/utils"; import { dateFormat } from "../utils/utils";
import { baseUrl } from "@/api/axios.js";
const props = defineProps(["books", "isSearch"]); const props = defineProps(["books", "isSearch"]);
const emit = defineEmits(["bookClick"]); const emit = defineEmits(["bookClick"]);
const handleClick = (book) => emit("bookClick", book); const handleClick = (book) => emit("bookClick", book);
const getCover = (coverUrl) => { const getCover = (coverUrl) => {
return /^data:/.test(coverUrl) return /^data:/.test(coverUrl)
? coverUrl ? coverUrl
: (import.meta.env.VITE_API || location.origin) + : baseUrl() + "/cover?path=" + encodeURIComponent(coverUrl);
"/cover?path=" +
encodeURIComponent(coverUrl);
}; };
const subJustify = computed(() => const subJustify = computed(() =>
@ -105,7 +104,7 @@ const subJustify = computed(() =>
margin-left: 20px; margin-left: 20px;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.name { .name {
width: fit-content; width: fit-content;
font-size: 16px; font-size: 16px;

View File

@ -57,12 +57,20 @@ export const useBookStore = defineStore("book", {
setConnectType(connectType) { setConnectType(connectType) {
this.connectType = connectType; this.connectType = connectType;
}, },
resetConnect() {
this.connectStatus = "正在连接后端服务器……";
this.connectType = "";
this.clearBooks();
},
setNewConnect(newConnect) { setNewConnect(newConnect) {
this.newConnect = newConnect; this.newConnect = newConnect;
}, },
addBooks(books) { addBooks(books) {
this.shelf = books; this.shelf = books;
}, },
clearBooks() {
this.shelf = [];
},
setCatalog(catalog) { setCatalog(catalog) {
this.catalog = catalog; this.catalog = catalog;
}, },

View File

@ -1,4 +1,5 @@
import { formatDate } from "@vueuse/shared"; import { formatDate } from "@vueuse/shared";
import { baseUrl } from "@/api/axios.js";
export const isLegadoUrl = (/** @type {string} */ url) => export const isLegadoUrl = (/** @type {string} */ url) =>
/,\s*\{/.test(url) || /,\s*\{/.test(url) ||
@ -12,7 +13,7 @@ export const isLegadoUrl = (/** @type {string} */ url) =>
*/ */
export function getImageFromLegado(src) { export function getImageFromLegado(src) {
return ( return (
(import.meta.env.VITE_API || location.origin) + baseUrl() +
"/image?path=" + "/image?path=" +
encodeURIComponent(src) + encodeURIComponent(src) +
"&url=" + "&url=" +

View File

@ -40,14 +40,24 @@
</div> </div>
<div class="setting-wrapper"> <div class="setting-wrapper">
<div class="setting-title">基本设定</div> <div class="setting-title">基本设定</div>
<div class="setting-item"> <div class="setting-ip">
<el-input
class="setting-input"
size="small"
:disabled="ipInput.disable"
v-model="ipInput.ip"
@keydown.enter="setIP"
/>
<el-tag <el-tag
:type="connectType" type="primary"
size="large" class="setting-toggle"
class="setting-connect" @click="toggleIpConfig"
:class="{ 'no-point': newConnect }"
@click="setIP"
> >
{{ ipInput.disable ? "修改" : "取消" }}
</el-tag>
</div>
<div class="setting-item">
<el-tag :type="connectType" size="large" class="setting-connect">
{{ connectStatus }} {{ connectStatus }}
</el-tag> </el-tag>
</div> </div>
@ -81,6 +91,7 @@ import githubUrl from "@/assets/imgs/github.png";
import { useLoading } from "@/hooks/loading"; import { useLoading } from "@/hooks/loading";
import { Search } from "@element-plus/icons-vue"; import { Search } from "@element-plus/icons-vue";
import API from "@api"; import API from "@api";
import { baseUrl, setRemoteIp } from "@/api/axios.js";
const store = useBookStore(); const store = useBookStore();
const { connectStatus, connectType, newConnect, shelf } = storeToRefs(store); const { connectStatus, connectType, newConnect, shelf } = storeToRefs(store);
@ -136,7 +147,7 @@ const searchBook = () => {
} }
try { try {
store.setSearchBooks(JSON.parse(data)); store.setSearchBooks(JSON.parse(data));
books.value = store.searchBooks books.value = store.searchBooks;
//store.searchBooks.forEach((item) => books.value.push(item)); //store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) { } catch (e) {
ElMessage.error("后端数据错误"); ElMessage.error("后端数据错误");
@ -152,7 +163,19 @@ const searchBook = () => {
); );
}; };
const setIP = () => {}; const ipInput = reactive({
ip: baseUrl(),
disable: true,
});
const toggleIpConfig = () => {
ipInput.ip = baseUrl();
ipInput.disable = !ipInput.disable;
};
const setIP = () => {
setRemoteIp(ipInput.ip);
ipInput.disable = true;
loadShelf();
};
const router = useRouter(); const router = useRouter();
const handleBookClick = async (book) => { const handleBookClick = async (book) => {
@ -195,13 +218,19 @@ onMounted(() => {
readingRecent.value.chapterIndex = 0; readingRecent.value.chapterIndex = 0;
} }
} }
loadShelf();
});
const loadShelf = () => {
store.resetConnect();
loadingWrapper( loadingWrapper(
store store
.saveBookProgress() .saveBookProgress()
// //
.finally(fetchBookShelfData), .finally(fetchBookShelfData),
); );
}); };
const fetchBookShelfData = () => { const fetchBookShelfData = () => {
return API.getBookShelf() return API.getBookShelf()
.then((response) => { .then((response) => {
@ -308,15 +337,31 @@ const fetchBookShelfData = () => {
font-family: FZZCYSK; font-family: FZZCYSK;
} }
.setting-ip {
margin-top: 16px;
white-space: nowrap;
}
.setting-input {
width: 216px;
margin-right: 4px;
}
.no-point { .no-point {
pointer-events: none; pointer-events: none;
} }
.setting-connect { .setting-toggle {
font-size: 8px; font-size: 10px;
margin-top: 16px;
// color: #6B7C87;
cursor: pointer; cursor: pointer;
//margin-top: 4px;
}
.setting-connect {
font-size: 10px;
margin-top: 4px;
// color: #6B7C87;
//cursor: pointer;
} }
} }