mirror of
https://github.com/gedoor/legado.git
synced 2025-08-10 00:52:30 +00:00
refactor(modules/web): Mirgrat to typescript; fix bugs
This commit is contained in:
219
modules/web/src/api/api.ts
Normal file
219
modules/web/src/api/api.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/** 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 */
|
||||
|
||||
import type { webReadConfig } from '@/web'
|
||||
import ajax from './axios'
|
||||
import type {
|
||||
BaseBook,
|
||||
Book,
|
||||
BookChapter,
|
||||
BookProgress,
|
||||
SeachBook,
|
||||
} from '@/book'
|
||||
import type { Source } from '@/source'
|
||||
|
||||
export type LeagdoApiResponse<T> = {
|
||||
isSuccess: boolean
|
||||
errorMsg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export let legado_http_entry_point = ''
|
||||
export let legado_webSocket_entry_point = ''
|
||||
|
||||
let wsOnError: typeof WebSocket.prototype.onerror = () => {}
|
||||
export const setWebsocketOnError = (
|
||||
callback: typeof WebSocket.prototype.onerror,
|
||||
) => {
|
||||
//WebSocket.prototype.onerror = callback
|
||||
wsOnError = callback
|
||||
}
|
||||
|
||||
export const setApiEntryPoint = (
|
||||
http_entry_point: string,
|
||||
webSocket_entry_point: string,
|
||||
) => {
|
||||
legado_http_entry_point = http_entry_point
|
||||
legado_webSocket_entry_point = webSocket_entry_point
|
||||
ajax.defaults.baseURL = http_entry_point.toString()
|
||||
}
|
||||
|
||||
// 书架API
|
||||
// Http
|
||||
const getReadConfig = async (http_url = legado_http_entry_point) => {
|
||||
const { data } = await ajax.get<LeagdoApiResponse<string>>('/getReadConfig', {
|
||||
baseURL: http_url.toString(),
|
||||
timeout: 3000,
|
||||
})
|
||||
if (data.isSuccess) {
|
||||
try {
|
||||
return JSON.parse(data.data) as webReadConfig
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const saveReadConfig = (config: webReadConfig) =>
|
||||
ajax.post<LeagdoApiResponse<string>>('/saveReadConfig', config)
|
||||
|
||||
const saveBookProgress = (bookProgress: BookProgress) =>
|
||||
ajax.post('/saveBookProgress', bookProgress)
|
||||
|
||||
const saveBookProgressWithBeacon = (bookProgress: BookProgress) => {
|
||||
if (!bookProgress) return
|
||||
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
|
||||
navigator.sendBeacon(
|
||||
new URL('/saveBookProgress', legado_http_entry_point),
|
||||
JSON.stringify(bookProgress),
|
||||
)
|
||||
}
|
||||
|
||||
const getBookShelf = () => ajax.get<LeagdoApiResponse<Book[]>>('/getBookshelf')
|
||||
|
||||
const getChapterList = (/** @type {string} */ bookUrl: string) =>
|
||||
ajax.get<LeagdoApiResponse<BookChapter[]>>(
|
||||
'/getChapterList?url=' + encodeURIComponent(bookUrl),
|
||||
)
|
||||
|
||||
const getBookContent = (
|
||||
/** @type {string} */ bookUrl: string,
|
||||
/** @type {number} */ chapterIndex: number,
|
||||
) =>
|
||||
ajax.get<LeagdoApiResponse<string>>(
|
||||
'/getBookContent?url=' +
|
||||
encodeURIComponent(bookUrl) +
|
||||
'&index=' +
|
||||
chapterIndex,
|
||||
)
|
||||
|
||||
// webSocket
|
||||
const search = (
|
||||
searchKey: string,
|
||||
onReceive: (data: SeachBook[]) => void,
|
||||
onFinish: () => void,
|
||||
) => {
|
||||
const socket = new WebSocket(
|
||||
new URL('/searchBook', legado_webSocket_entry_point),
|
||||
)
|
||||
socket.onerror = wsOnError
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(`{"key":"${searchKey}"}`)
|
||||
}
|
||||
socket.onmessage = ({ data }) => {
|
||||
try {
|
||||
onReceive(JSON.parse(data))
|
||||
} catch {
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
const saveBook = (book: BaseBook) =>
|
||||
ajax.post<LeagdoApiResponse<string>>('/saveBook', book)
|
||||
const deleteBook = (book: BaseBook) =>
|
||||
ajax.post<LeagdoApiResponse<string>>('/deleteBook', book)
|
||||
|
||||
const isBookSource = /bookSource/i.test(location.href)
|
||||
|
||||
// 源编辑API
|
||||
// Http
|
||||
const getSources = () =>
|
||||
isBookSource ? ajax.get('/getBookSources') : ajax.get('/getRssSources')
|
||||
|
||||
const saveSource = (data: Source) =>
|
||||
isBookSource
|
||||
? ajax.post<LeagdoApiResponse<string>>('/saveBookSource', data)
|
||||
: ajax.post<LeagdoApiResponse<string>>('/saveRssSource', data)
|
||||
|
||||
const saveSources = (data: Source[]) =>
|
||||
isBookSource
|
||||
? ajax.post<LeagdoApiResponse<Source[]>>('/saveBookSources', data)
|
||||
: ajax.post<LeagdoApiResponse<Source[]>>('/saveRssSources', data)
|
||||
|
||||
const deleteSource = (data: Source[]) =>
|
||||
isBookSource
|
||||
? ajax.post<LeagdoApiResponse<string>>('/deleteBookSources', data)
|
||||
: ajax.post<LeagdoApiResponse<string>>('/deleteRssSources', data)
|
||||
|
||||
// webSocket
|
||||
const debug = (
|
||||
/** @type {string} */ sourceUrl: string,
|
||||
/** @type {string} */ searchKey: string,
|
||||
/** @type {(data: string) => void} */ onReceive: (data: string) => void,
|
||||
/** @type {() => void} */ onFinish: () => void,
|
||||
) => {
|
||||
const url = new URL(
|
||||
`/${isBookSource ? 'bookSource' : 'rssSource'}Debug`,
|
||||
legado_webSocket_entry_point,
|
||||
)
|
||||
|
||||
const socket = new WebSocket(url)
|
||||
socket.onerror = wsOnError
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey }))
|
||||
}
|
||||
socket.onmessage = ({ data }) => onReceive(data)
|
||||
|
||||
socket.onclose = () => {
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从阅读获取需要特定处理的书籍封面
|
||||
* @param {string} coverUrl
|
||||
*/
|
||||
const getProxyCoverUrl = (coverUrl: string) => {
|
||||
if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl
|
||||
return new URL(
|
||||
'/cover?path=' + encodeURIComponent(coverUrl),
|
||||
legado_http_entry_point,
|
||||
).toString()
|
||||
}
|
||||
/**
|
||||
* 从阅读获取需要特定处理的图片
|
||||
* @param {string} bookUrl
|
||||
* @param {string} src
|
||||
* @param {number|`${number}`} width
|
||||
*/
|
||||
const getProxyImageUrl = (
|
||||
bookUrl: string,
|
||||
src: string,
|
||||
width: number | `${number}`,
|
||||
) => {
|
||||
if (src.startsWith(legado_http_entry_point)) return src
|
||||
return new URL(
|
||||
'/image?path=' +
|
||||
encodeURIComponent(src) +
|
||||
'&url=' +
|
||||
encodeURIComponent(bookUrl) +
|
||||
'&width=' +
|
||||
width,
|
||||
legado_http_entry_point,
|
||||
).toString()
|
||||
}
|
||||
|
||||
export default {
|
||||
getReadConfig,
|
||||
saveReadConfig,
|
||||
saveBookProgress,
|
||||
saveBookProgressWithBeacon,
|
||||
getBookShelf,
|
||||
getChapterList,
|
||||
getBookContent,
|
||||
search,
|
||||
saveBook,
|
||||
deleteBook,
|
||||
|
||||
getSources,
|
||||
saveSources,
|
||||
saveSource,
|
||||
deleteSource,
|
||||
debug,
|
||||
|
||||
getProxyCoverUrl,
|
||||
getProxyImageUrl,
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from "axios";
|
||||
import axios from 'axios'
|
||||
|
||||
/** @type {string} localStorage保存自定义阅读http服务接口的键值 */
|
||||
export const baseURL_localStorage_key = "remoteUrl"
|
||||
const SECOND = 1000;
|
||||
export const baseURL_localStorage_key = 'remoteUrl'
|
||||
const SECOND = 1000
|
||||
|
||||
const ajax = axios.create({
|
||||
baseURL:
|
||||
@@ -10,6 +10,6 @@ const ajax = axios.create({
|
||||
localStorage.getItem(baseURL_localStorage_key) ||
|
||||
location.origin,
|
||||
timeout: 120 * SECOND,
|
||||
});
|
||||
})
|
||||
|
||||
export default ajax;
|
||||
export default ajax
|
||||
@@ -1,266 +0,0 @@
|
||||
import ajax from "./axios";
|
||||
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/web */
|
||||
|
||||
/**@type string */
|
||||
export let legado_http_entry_point = "";
|
||||
/**@type string */
|
||||
export let legado_webSocket_entry_point = "";
|
||||
|
||||
/**
|
||||
* @param {string|URL} http_url
|
||||
* @returns {URL}
|
||||
* @throws {Error}
|
||||
*/
|
||||
export const validatorHttpUrl = (http_url) => {
|
||||
try {
|
||||
const url = new URL(http_url);
|
||||
if (url.toString() === legado_http_entry_point)
|
||||
throw new Error("Please input different url: " + legado_http_entry_point);
|
||||
const { protocol } = url;
|
||||
if (!protocol.startsWith("http"))
|
||||
throw new Error("Expect http:/https: protocol but " + protocol);
|
||||
return url;
|
||||
} catch (e) {
|
||||
if (localStorage.getItem("remoteUrl") == http_url) {
|
||||
localStorage.removeItem("remoteUrl");
|
||||
console.warn("Remove remoteUrl from localStorage");
|
||||
}
|
||||
throw new Error("Fail to parse Leagdo remoteUrl " + http_url, { cause: e });
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @param {string|URL} http_url
|
||||
* @returns
|
||||
*/
|
||||
export const setLeagdoHttpUrl = (http_url) => {
|
||||
let url = new URL(location.origin); //默认当前网址的origin部分
|
||||
try {
|
||||
url = validatorHttpUrl(http_url);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
console.info(
|
||||
"setLeagdoHttpUrl: FallBack to location.origin: " + location.origin,
|
||||
);
|
||||
}
|
||||
const { protocol, port } = url;
|
||||
// websocket服务端口 为http服务端口 + 1
|
||||
let legado_webSocket_port, legado_webSocket_protocol;
|
||||
if (port !== "") {
|
||||
legado_webSocket_port = String(Number(port) + 1);
|
||||
} else {
|
||||
legado_webSocket_port = protocol.startsWith("https:") ? "444" : "81";
|
||||
}
|
||||
// websocket协议是否为加密版本
|
||||
legado_webSocket_protocol = protocol.startsWith("https:")
|
||||
? "wss://"
|
||||
: "ws://";
|
||||
|
||||
ajax.defaults.baseURL = url.toString();
|
||||
legado_http_entry_point = url.toString();
|
||||
|
||||
url.protocol = legado_webSocket_protocol;
|
||||
url.port = legado_webSocket_port;
|
||||
legado_webSocket_entry_point = url.toString();
|
||||
|
||||
console.info("legado_api_config:");
|
||||
console.table({
|
||||
"http API入口": legado_http_entry_point,
|
||||
"webSocket API入口": legado_webSocket_entry_point,
|
||||
});
|
||||
};
|
||||
|
||||
// 手动初始化 阅读web服务地址
|
||||
setLeagdoHttpUrl(ajax.defaults.baseURL);
|
||||
/**
|
||||
* @param {string|URL|undefined} http_url 不传为当前阅读HTTP服务接口
|
||||
* @returns
|
||||
*/
|
||||
const testLeagdoHttpUrlConnection = async (http_url = legado_http_entry_point) => {
|
||||
const { data = {} } = await ajax.get("/getReadConfig", {
|
||||
baseURL: http_url.toString(),
|
||||
timeout: 3000,
|
||||
});
|
||||
// 返回结果应该是JSON 并有键值isSuccess
|
||||
try {
|
||||
if ("isSuccess" in data) return data.data;
|
||||
throw new Error("ReadConfig后端返回格式错误");
|
||||
} catch {
|
||||
throw new Error("ReadConfig后端返回格式错误");
|
||||
}
|
||||
};
|
||||
|
||||
const isSourecEditor = /source/i.test(location.href);
|
||||
const APIExceptionHandler = (error) => {
|
||||
if (isSourecEditor) {
|
||||
ElMessage({
|
||||
message: "后端错误,检查网络或者阅读app",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
|
||||
ajax.interceptors.response.use((response) => response, APIExceptionHandler);
|
||||
|
||||
// 书架API
|
||||
// Http
|
||||
/** @returns {Promise<import("axios").AxiosResponse<{isSuccess: boolean, data: string, errorMsg:string}>>} */
|
||||
const getReadConfig = () => ajax.get("/getReadConfig", { timeout: 3000 });
|
||||
const saveReadConfig = (config) => ajax.post("/saveReadConfig", config);
|
||||
|
||||
const saveBookProgress = (bookProgress) =>
|
||||
ajax.post("/saveBookProgress", bookProgress);
|
||||
|
||||
const saveBookProgressWithBeacon = (bookProgress) => {
|
||||
if (!bookProgress) return;
|
||||
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
|
||||
navigator.sendBeacon(
|
||||
new URL("/saveBookProgress", legado_http_entry_point),
|
||||
JSON.stringify(bookProgress),
|
||||
);
|
||||
};
|
||||
|
||||
const getBookShelf = () => ajax.get("/getBookshelf");
|
||||
|
||||
const getChapterList = (/** @type {string} */ bookUrl) =>
|
||||
ajax.get("/getChapterList?url=" + encodeURIComponent(bookUrl));
|
||||
|
||||
const getBookContent = (
|
||||
/** @type {string} */ bookUrl,
|
||||
/** @type {number} */ chapterIndex,
|
||||
) =>
|
||||
ajax.get(
|
||||
"/getBookContent?url=" +
|
||||
encodeURIComponent(bookUrl) +
|
||||
"&index=" +
|
||||
chapterIndex,
|
||||
);
|
||||
|
||||
// webSocket
|
||||
const search = (
|
||||
/** @type {string} */ searchKey,
|
||||
/** @type {(data: string) => void} */ onReceive,
|
||||
/** @type {() => void} */ onFinish,
|
||||
) => {
|
||||
const socket = new WebSocket(
|
||||
new URL("/searchBook", legado_webSocket_entry_point),
|
||||
);
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(`{"key":"${searchKey}"}`);
|
||||
};
|
||||
socket.onmessage = ({ data }) => onReceive(data);
|
||||
|
||||
socket.onclose = () => {
|
||||
onFinish();
|
||||
};
|
||||
};
|
||||
|
||||
const saveBook = (book) => ajax.post("/saveBook", book);
|
||||
const deleteBook = (book) => ajax.post("/deleteBook", book);
|
||||
|
||||
const isBookSource = /bookSource/i.test(location.href);
|
||||
|
||||
// 源编辑API
|
||||
// Http
|
||||
const getSources = () =>
|
||||
isBookSource ? ajax.get("/getBookSources") : ajax.get("/getRssSources");
|
||||
|
||||
const saveSource = (data) =>
|
||||
isBookSource
|
||||
? ajax.post("/saveBookSource", data)
|
||||
: ajax.post("/saveRssSource", data);
|
||||
|
||||
const saveSources = (data) =>
|
||||
isBookSource
|
||||
? ajax.post("/saveBookSources", data)
|
||||
: ajax.post("/saveRssSources", data);
|
||||
|
||||
const deleteSource = (data) =>
|
||||
isBookSource
|
||||
? ajax.post("/deleteBookSources", data)
|
||||
: ajax.post("/deleteRssSources", data);
|
||||
|
||||
// webSocket
|
||||
const debug = (
|
||||
/** @type {string} */ sourceUrl,
|
||||
/** @type {string} */ searchKey,
|
||||
/** @type {(data: string) => void} */ onReceive,
|
||||
/** @type {() => void} */ onFinish,
|
||||
) => {
|
||||
const url = new URL(
|
||||
`/${isBookSource ? "bookSource" : "rssSource"}Debug`,
|
||||
legado_webSocket_entry_point,
|
||||
);
|
||||
|
||||
const socket = new WebSocket(url);
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey }));
|
||||
};
|
||||
socket.onmessage = ({ data }) => onReceive(data);
|
||||
|
||||
socket.onclose = () => {
|
||||
ElMessage({
|
||||
message: "调试已关闭!",
|
||||
type: "info",
|
||||
});
|
||||
onFinish();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 从阅读获取需要特定处理的书籍封面
|
||||
* @param {string} coverUrl
|
||||
*/
|
||||
const getProxyCoverUrl = (coverUrl) => {
|
||||
if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl;
|
||||
return new URL(
|
||||
"/cover?path=" + encodeURIComponent(coverUrl),
|
||||
legado_http_entry_point,
|
||||
).toString();
|
||||
};
|
||||
/**
|
||||
* 从阅读获取需要特定处理的图片
|
||||
* @param {string} src
|
||||
* @param {number|`${number}`} width
|
||||
*/
|
||||
const getProxyImageUrl = (src, width) => {
|
||||
if (src.startsWith(legado_http_entry_point)) return src;
|
||||
return new URL(
|
||||
"/image?path=" +
|
||||
encodeURIComponent(src) +
|
||||
"&url=" +
|
||||
encodeURIComponent(sessionStorage.getItem("bookUrl")) +
|
||||
"&width=" +
|
||||
width,
|
||||
legado_http_entry_point,
|
||||
).toString();
|
||||
};
|
||||
|
||||
export default {
|
||||
getReadConfig,
|
||||
saveReadConfig,
|
||||
saveBookProgress,
|
||||
saveBookProgressWithBeacon,
|
||||
getBookShelf,
|
||||
getChapterList,
|
||||
getBookContent,
|
||||
search,
|
||||
saveBook,
|
||||
deleteBook,
|
||||
|
||||
getSources,
|
||||
saveSources,
|
||||
saveSource,
|
||||
deleteSource,
|
||||
debug,
|
||||
|
||||
getProxyCoverUrl,
|
||||
getProxyImageUrl,
|
||||
|
||||
testLeagdoHttpUrlConnection,
|
||||
};
|
||||
96
modules/web/src/api/index.ts
Normal file
96
modules/web/src/api/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import type { LeagdoApiResponse } from './api'
|
||||
import API, {
|
||||
setWebsocketOnError,
|
||||
legado_http_entry_point,
|
||||
legado_webSocket_entry_point,
|
||||
setApiEntryPoint,
|
||||
} from './api'
|
||||
import ajax from './axios'
|
||||
import { validatorHttpUrl } from '@/utils/utils'
|
||||
|
||||
const LeagdoApiResponseKeys: string[] = Array.of('isSuccess', 'errorMsg')
|
||||
|
||||
/** Axios.Interceptor: check if resp is LeagaoLeagdoApiResponse*/
|
||||
const responseCheckInterceptor = (resp: AxiosResponse) => {
|
||||
let isLeagdoApiResponse = true
|
||||
try {
|
||||
const data = resp.data
|
||||
|
||||
for (const key of LeagdoApiResponseKeys) {
|
||||
if (!(key in data)) {
|
||||
isLeagdoApiResponse = false
|
||||
LeagdoApiResponseKeys.length = 0
|
||||
}
|
||||
}
|
||||
if ((data as LeagdoApiResponse<unknown>).isSuccess === true) {
|
||||
if (!('data' in data)) {
|
||||
isLeagdoApiResponse = false
|
||||
LeagdoApiResponseKeys.length = 0
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
isLeagdoApiResponse = false
|
||||
}
|
||||
if (isLeagdoApiResponse === false) {
|
||||
ElNotification.warning('后端返回内容格式错误')
|
||||
throw new Error()
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
const axiosErrorInterceptor = (err: unknown) => {
|
||||
ElNotification.error('后端连接失败,请检查阅读WEB服务或者设置其它可用链接')
|
||||
throw err
|
||||
}
|
||||
// http全局
|
||||
ajax.interceptors.response.use(responseCheckInterceptor, axiosErrorInterceptor)
|
||||
// websocket
|
||||
setWebsocketOnError(axiosErrorInterceptor)
|
||||
|
||||
/**
|
||||
* 按照阅读的默认规则 解析阅读HTTP WebSocket API入口地址
|
||||
* @returns [http_url, webSocekt_url]
|
||||
*/
|
||||
export const parseLeagdoHttpUrlWithDefault = (
|
||||
http_url: string | URL,
|
||||
): [string, string] => {
|
||||
let url = new URL(location.origin) //默认当前网址的origin部分
|
||||
if (validatorHttpUrl(http_url)) {
|
||||
url = new URL(http_url)
|
||||
}
|
||||
const { protocol, port } = url
|
||||
// websocket服务端口 为http服务端口 + 1
|
||||
let legado_webSocket_port
|
||||
if (port !== '') {
|
||||
legado_webSocket_port = String(Number(port) + 1)
|
||||
} else {
|
||||
legado_webSocket_port = protocol.startsWith('https:') ? '444' : '81'
|
||||
}
|
||||
// websocket协议是否为加密版本
|
||||
const legado_webSocket_protocol = protocol.startsWith('https:')
|
||||
? 'wss://'
|
||||
: 'ws://'
|
||||
|
||||
const http_entry_point = url.toString()
|
||||
|
||||
url.protocol = legado_webSocket_protocol
|
||||
url.port = legado_webSocket_port
|
||||
const webSocket_entry_point = url.toString()
|
||||
|
||||
console.info('legado_api_config:')
|
||||
console.table({
|
||||
'http API入口': legado_http_entry_point,
|
||||
'webSocket API入口': legado_webSocket_entry_point,
|
||||
})
|
||||
return [http_entry_point, webSocket_entry_point]
|
||||
}
|
||||
|
||||
//export const useLeagdoRemoteUrlDialog = () => { }
|
||||
|
||||
setApiEntryPoint(
|
||||
...parseLeagdoHttpUrlWithDefault(ajax.defaults.baseURL as string),
|
||||
)
|
||||
|
||||
export default API
|
||||
export * from './api'
|
||||
Reference in New Issue
Block a user