refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc * feat: Overhaul Playground UI * feat: Add Filters to New UI * feat: Persist Selected Plugin * feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts [skip ci] * feat: UX Improvements * chore: Update Plugin Path in Publish Workflow * fix: Add Backward Compatibility * chore: Update Plugin Publishing Script
This commit is contained in:
committed by
GitHub
parent
252beff0bf
commit
20c2b75b01
@@ -0,0 +1,156 @@
|
||||
/* global Buffer, RequestInit */
|
||||
|
||||
import { parse as parseProto } from 'protobufjs';
|
||||
|
||||
type FetchInit = {
|
||||
headers?: Record<string, string | undefined> | Headers;
|
||||
method?: string;
|
||||
body?: FormData | string;
|
||||
[x: string]:
|
||||
| string
|
||||
| Record<string, string | undefined>
|
||||
| undefined
|
||||
| FormData
|
||||
| Headers;
|
||||
};
|
||||
|
||||
const makeInit = async (init?: FetchInit) => {
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
'Connection': 'keep-alive',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': '*',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
};
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
for (const [name, value] of Object.entries(defaultHeaders)) {
|
||||
if (!init.headers.get(name)) init.headers.set(name, value);
|
||||
}
|
||||
} else {
|
||||
init.headers = {
|
||||
...defaultHeaders,
|
||||
...init.headers,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
init = {
|
||||
...init,
|
||||
headers: defaultHeaders,
|
||||
};
|
||||
}
|
||||
return init;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch with (Android) User Agent
|
||||
* @param url
|
||||
* @param init
|
||||
* @returns response as normal fetch
|
||||
*/
|
||||
export async function fetchApi(url: string, init?: FetchInit) {
|
||||
init = await makeInit(init);
|
||||
console.log(url, init);
|
||||
return await fetch(url, init as RequestInit);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @param init
|
||||
* @returns base64 string of file
|
||||
* @example fetchFile('https://avatars.githubusercontent.com/u/81222734?s=48&v=4');
|
||||
*/
|
||||
export const fetchFile = async function (url: string, init?: FetchInit) {
|
||||
init = await makeInit(init);
|
||||
console.log(url, init);
|
||||
try {
|
||||
const res = await fetch(url, init as RequestInit);
|
||||
if (!res.ok) return '';
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer).toString('base64');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @param init
|
||||
* @param encoding default: `utf-8`. link: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/encoding
|
||||
* @returns plain text
|
||||
* @example fetchText('https://github.com/LNReader/lnreader', {}, 'gbk');
|
||||
*/
|
||||
export const fetchText = async function (
|
||||
url: string,
|
||||
init?: FetchInit,
|
||||
encoding?: string,
|
||||
): Promise<string> {
|
||||
init = await makeInit(init);
|
||||
console.log(url, init);
|
||||
try {
|
||||
const res = await fetch(url, init as RequestInit);
|
||||
if (!res.ok) return '';
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const decoder = new TextDecoder(encoding);
|
||||
return decoder.decode(arrayBuffer);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type ProtoRequestInit = {
|
||||
// merged .proto file
|
||||
proto: string;
|
||||
requestType: string;
|
||||
requestData?: any;
|
||||
responseType: string;
|
||||
};
|
||||
|
||||
const BYTE_MARK = BigInt((1 << 8) - 1);
|
||||
|
||||
export const fetchProto = async function <ReturnType>(
|
||||
protoInit: ProtoRequestInit,
|
||||
url: string,
|
||||
init?: FetchInit,
|
||||
) {
|
||||
const protoRoot = parseProto(protoInit.proto).root;
|
||||
const RequestMessge = protoRoot.lookupType(protoInit.requestType);
|
||||
if (RequestMessge.verify(protoInit.requestData)) {
|
||||
throw new Error('Invalid Proto');
|
||||
}
|
||||
// encode request data
|
||||
const encodedrequest = RequestMessge.encode(protoInit.requestData).finish();
|
||||
const requestLength = BigInt(encodedrequest.length);
|
||||
const headers = new Uint8Array(
|
||||
Array(5)
|
||||
.fill(0)
|
||||
.map((v, idx) => {
|
||||
if (idx === 0) return 0;
|
||||
return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK);
|
||||
}),
|
||||
);
|
||||
init = await makeInit(init);
|
||||
const bodyArray = new Uint8Array(headers.length + encodedrequest.length);
|
||||
bodyArray.set(headers, 0);
|
||||
bodyArray.set(encodedrequest, headers.length);
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
...init,
|
||||
body: bodyArray,
|
||||
} as RequestInit)
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(arr => {
|
||||
// decode response data
|
||||
const payload = new Uint8Array(arr);
|
||||
const length = Number(
|
||||
BigInt(payload[1] << 24) |
|
||||
BigInt(payload[2] << 16) |
|
||||
BigInt(payload[3] << 8) |
|
||||
BigInt(payload[4]),
|
||||
);
|
||||
const ResponseMessage = protoRoot.lookupType(protoInit.responseType);
|
||||
return ResponseMessage.decode(payload.slice(5, 5 + length));
|
||||
}) as ReturnType;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
class Storage {
|
||||
private db: Record<string, { created: Date; value: any; expires?: number }>;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Storage class.
|
||||
*/
|
||||
constructor() {
|
||||
this.db = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a key-value pair in storage.
|
||||
*
|
||||
* @param {string} key - The key to set.
|
||||
* @param {any} value - The value to set.
|
||||
* @param {Date | number} [expires] - Optional expiry date or time in milliseconds.
|
||||
*/
|
||||
set(key: string, value: any, expires?: Date | number): void {
|
||||
this.db[key] = {
|
||||
created: new Date(),
|
||||
value,
|
||||
expires: expires instanceof Date ? expires.getTime() : expires,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value for a given key from storage.
|
||||
*
|
||||
* @param {string} key - The key to retrieve the value for.
|
||||
* @param {boolean} [raw] - Optional flag to return the raw stored item.
|
||||
* @returns {any} The stored value or undefined if key is not found.
|
||||
*/
|
||||
get(key: string, raw?: boolean): any {
|
||||
const item = this.db[key];
|
||||
if (item?.expires && Date.now() > item.expires) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return raw ? item : item?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all keys set by the `set` method.
|
||||
*
|
||||
* @returns {string[]} An array of keys.
|
||||
*/
|
||||
getAllKeys(): string[] {
|
||||
return Object.keys(this.db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a key from the storage.
|
||||
*
|
||||
* @param key - The key to delete.
|
||||
*/
|
||||
delete(key: string): void {
|
||||
delete this.db[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored items from storage.
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.db = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance of the Storage class
|
||||
export const storage = new Storage();
|
||||
|
||||
/*
|
||||
These parameters cannot be implemented in `test-web`.
|
||||
They are generated in the browser when js-scripts are executed
|
||||
Read more
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the structure of a storage object with string keys and values.
|
||||
*/
|
||||
type StorageObject = Record<string, any>;
|
||||
|
||||
/**
|
||||
* Represents a simplified version of the browser's localStorage.
|
||||
*/
|
||||
class LocalStorage {
|
||||
private db: StorageObject;
|
||||
|
||||
constructor() {
|
||||
this.db = {};
|
||||
}
|
||||
|
||||
get(): StorageObject | undefined {
|
||||
return this.db;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instances of LocalStorage and sessionStorage
|
||||
export const localStorage = new LocalStorage();
|
||||
export const sessionStorage = new LocalStorage();
|
||||
@@ -0,0 +1,36 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Merges Tailwind CSS classes with clsx
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is absolute
|
||||
*/
|
||||
export const isUrlAbsolute = (url: string) => {
|
||||
if (url) {
|
||||
if (url.indexOf('//') === 0) {
|
||||
return true;
|
||||
} // URL is protocol-relative (= absolute)
|
||||
if (url.indexOf('://') === -1) {
|
||||
return false;
|
||||
} // URL has no protocol (= relative)
|
||||
if (url.indexOf('.') === -1) {
|
||||
return false;
|
||||
} // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
|
||||
if (url.indexOf('/') === -1) {
|
||||
return false;
|
||||
} // URL does not contain a single slash (= relative)
|
||||
if (url.indexOf(':') > url.indexOf('/')) {
|
||||
return false;
|
||||
} // The first colon comes after the first slash (= relative)
|
||||
if (url.indexOf('://') < url.indexOf('.')) {
|
||||
return true;
|
||||
} // Protocol is defined before first dot (= absolute)
|
||||
}
|
||||
return false; // Anything else must be relative
|
||||
};
|
||||
Reference in New Issue
Block a user