mirror of https://github.com/VERT-sh/VERT.git
fix: code review suggestions
yes used ai for this, all seems valid and noted though
This commit is contained in:
parent
8729584614
commit
d3aeb9b696
36
bun.lock
36
bun.lock
|
|
@ -11,49 +11,49 @@
|
|||
"@fontsource/lexend": "^5.2.11",
|
||||
"@fontsource/radio-canada-big": "^5.2.7",
|
||||
"@imagemagick/magick-wasm": "^0.0.37",
|
||||
"@mediabunny/ac3": "^1.35.1",
|
||||
"@mediabunny/flac-encoder": "^1.37.0",
|
||||
"@mediabunny/mp3-encoder": "^1.35.1",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"@mediabunny/ac3": "^1.40.0",
|
||||
"@mediabunny/flac-encoder": "^1.40.0",
|
||||
"@mediabunny/mp3-encoder": "^1.40.0",
|
||||
"@stripe/stripe-js": "^8.11.0",
|
||||
"byte-data": "^19.0.1",
|
||||
"client-zip": "^2.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"lucide-svelte": "^0.554.0",
|
||||
"mediabunny": "^1.37.0",
|
||||
"music-metadata": "^11.12.0",
|
||||
"mediabunny": "^1.40.0",
|
||||
"music-metadata": "^11.12.3",
|
||||
"overlayscrollbars": "^2.14.0",
|
||||
"overlayscrollbars-svelte": "^0.5.5",
|
||||
"p-queue": "^9.1.0",
|
||||
"riff-file": "^1.0.3",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sanitize-html": "^2.17.2",
|
||||
"svelte-stripe": "^1.4.0",
|
||||
"vert-wasm": "^0.0.2",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vite-plugin-wasm": "^3.6.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.11.0",
|
||||
"@inlang/paraglide-js": "^2.15.0",
|
||||
"@poppanator/sveltekit-svg": "^5.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.52.0",
|
||||
"@sveltejs/kit": "^2.55.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"css-select": "5.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.15.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"sass": "^1.97.3",
|
||||
"svelte": "^5.51.2",
|
||||
"svelte-check": "^4.4.0",
|
||||
"sass": "^1.98.0",
|
||||
"svelte": "^5.54.0",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.55.0",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"lint": "prettier --check .; p=$?; eslint .; e=$?; [ $p -eq 0 ] && [ $e -eq 0 ]"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.15.0",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,25 @@
|
|||
type Props = DialogProps<VertdErrorDetailsProps>;
|
||||
|
||||
let { additional }: Props = $props();
|
||||
let errorBlobUrl = $state("");
|
||||
|
||||
$effect(() => {
|
||||
if (!additional.errorMessage) {
|
||||
errorBlobUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const nextUrl = URL.createObjectURL(
|
||||
new Blob([additional.errorMessage], {
|
||||
type: "text/plain",
|
||||
}),
|
||||
);
|
||||
errorBlobUrl = nextUrl;
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(nextUrl);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
@ -41,13 +60,7 @@
|
|||
{@html sanitize(link(
|
||||
["view_link"],
|
||||
m["convert.errors.vertd_details_error_message"](),
|
||||
[
|
||||
URL.createObjectURL(
|
||||
new Blob([additional.errorMessage], {
|
||||
type: "text/plain",
|
||||
}),
|
||||
),
|
||||
],
|
||||
[errorBlobUrl || "#"],
|
||||
[true],
|
||||
["text-blue-500 font-normal"],
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -90,10 +90,11 @@ export class FFmpegConverter extends Converter {
|
|||
this.error = (msg) => error(["converters", this.name], msg);
|
||||
this.log(`created converter`);
|
||||
if (!browser) return;
|
||||
try {
|
||||
|
||||
// this is just to cache the wasm and js for when we actually use it. we're not using this ffmpeg instance
|
||||
this.ffmpeg = new FFmpeg();
|
||||
(async () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const baseURL =
|
||||
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
|
||||
|
||||
|
|
@ -105,7 +106,6 @@ export class FFmpegConverter extends Converter {
|
|||
});
|
||||
|
||||
this.status = "ready";
|
||||
})();
|
||||
} catch (err) {
|
||||
this.error(`Error loading ffmpeg: ${err}`);
|
||||
this.status = "error";
|
||||
|
|
@ -114,6 +114,7 @@ export class FFmpegConverter extends Converter {
|
|||
message: m["workers.errors.ffmpeg"](),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
public async getAvailableSettings(): Promise<SettingDefinition[]> {
|
||||
|
|
@ -251,6 +252,7 @@ export class FFmpegConverter extends Converter {
|
|||
|
||||
ffmpeg.on("log", errorListener);
|
||||
|
||||
try {
|
||||
const buf = new Uint8Array(await input.file.arrayBuffer());
|
||||
await ffmpeg.writeFile("input", buf);
|
||||
this.log(`wrote ${input.name} to ffmpeg virtual fs`);
|
||||
|
|
@ -266,31 +268,26 @@ export class FFmpegConverter extends Converter {
|
|||
await ffmpeg.exec(command);
|
||||
this.log("executed ffmpeg command");
|
||||
|
||||
if (conversionError) {
|
||||
ffmpeg.off("log", errorListener);
|
||||
ffmpeg.terminate();
|
||||
throw new Error(conversionError);
|
||||
}
|
||||
if (conversionError) throw new Error(conversionError);
|
||||
|
||||
const output = (await ffmpeg.readFile(
|
||||
"output" + to,
|
||||
)) as unknown as Uint8Array;
|
||||
|
||||
if (!output || output.length === 0) {
|
||||
ffmpeg.off("log", errorListener);
|
||||
ffmpeg.terminate();
|
||||
if (!output || output.length === 0)
|
||||
throw new Error("empty file returned");
|
||||
}
|
||||
|
||||
const outputFileName =
|
||||
input.name.split(".").slice(0, -1).join(".") + to;
|
||||
this.log(`read ${outputFileName} from ffmpeg virtual fs`);
|
||||
|
||||
ffmpeg.off("log", errorListener);
|
||||
ffmpeg.terminate();
|
||||
|
||||
const outBuf = new Uint8Array(output).buffer.slice(0);
|
||||
return new VertFile(new File([outBuf], outputFileName), to);
|
||||
} finally {
|
||||
ffmpeg.off("log", errorListener);
|
||||
this.activeConversions.delete(input.id);
|
||||
ffmpeg.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
|
|
@ -529,7 +526,7 @@ export class FFmpegConverter extends Converter {
|
|||
// -map for each audio track
|
||||
if (settings.tracks > 1) {
|
||||
for (let i = 0; i < settings.tracks; i++) {
|
||||
tracksArgs.push("-map", `0:a:${i - 1}`);
|
||||
tracksArgs.push("-map", `0:a:${i}`);
|
||||
}
|
||||
} else {
|
||||
tracksArgs = ["-map", "0:a:0"]; // default to first audio track if not specified
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export class PandocConverter extends Converter {
|
|||
|
||||
this.activeConversions.set(file.id, worker);
|
||||
|
||||
try {
|
||||
const loadMsg: WorkerMessage = {
|
||||
type: "load",
|
||||
wasm: this.wasm,
|
||||
|
|
@ -71,8 +72,6 @@ export class PandocConverter extends Converter {
|
|||
worker.postMessage(convertMsg);
|
||||
const result = await waitForMessage(worker);
|
||||
if (result.type === "error") {
|
||||
worker.terminate();
|
||||
// throw new Error(result.error);
|
||||
const error = result.error.toString();
|
||||
switch (result.errorKind) {
|
||||
case "PandocUnknownReaderError": {
|
||||
|
|
@ -106,12 +105,14 @@ export class PandocConverter extends Converter {
|
|||
}
|
||||
|
||||
if (!to.startsWith(".")) to = `.${to}`;
|
||||
this.activeConversions.delete(file.id);
|
||||
worker.terminate();
|
||||
return new VertFile(
|
||||
new File([result.output], file.name),
|
||||
result.isZip ? ".zip" : to,
|
||||
);
|
||||
} finally {
|
||||
this.activeConversions.delete(file.id);
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -723,11 +723,42 @@ export class VertdConverter extends Converter {
|
|||
const apiUrl = await VertdInstance.instance.url();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const protocol = apiUrl.startsWith("https") ? "wss:" : "ws:";
|
||||
const ws = new WebSocket(
|
||||
`${protocol}//${apiUrl.replace("http://", "").replace("https://", "")}/api/ws`,
|
||||
);
|
||||
|
||||
const connectTimeout = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
this.activeConversions.delete(input.id);
|
||||
ws.close();
|
||||
reject(new Error("vertd websocket connection timeout"));
|
||||
}, 30000);
|
||||
|
||||
const rejectConversion = (reason: unknown) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(connectTimeout);
|
||||
this.activeConversions.delete(input.id);
|
||||
if (
|
||||
ws.readyState === WebSocket.CONNECTING ||
|
||||
ws.readyState === WebSocket.OPEN
|
||||
) {
|
||||
ws.close();
|
||||
}
|
||||
reject(reason);
|
||||
};
|
||||
|
||||
const resolveConversion = (value: VertFile) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(connectTimeout);
|
||||
this.activeConversions.delete(input.id);
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
this.activeConversions.set(input.id, {
|
||||
ws,
|
||||
jobId: uploadRes.id,
|
||||
|
|
@ -735,6 +766,7 @@ export class VertdConverter extends Converter {
|
|||
});
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(connectTimeout);
|
||||
this.log(
|
||||
`opened ws connection to vertd for file ${input.name}`,
|
||||
);
|
||||
|
|
@ -752,8 +784,32 @@ export class VertdConverter extends Converter {
|
|||
this.log(`sent startJob message for file ${input.name}`);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
this.error(`ws error for file ${input.name}`);
|
||||
rejectConversion(new Error("vertd websocket error"));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (settled) return;
|
||||
this.error(
|
||||
`ws closed unexpectedly for file ${input.name} (code: ${event.code})`,
|
||||
);
|
||||
rejectConversion(new Error("vertd websocket closed unexpectedly"));
|
||||
};
|
||||
|
||||
ws.onmessage = async (e) => {
|
||||
const msg: VertdMessage = JSON.parse(e.data);
|
||||
let msg: VertdMessage;
|
||||
try {
|
||||
if (typeof e.data !== "string") {
|
||||
rejectConversion(new Error("invalid websocket payload type"));
|
||||
return;
|
||||
}
|
||||
msg = JSON.parse(e.data);
|
||||
} catch {
|
||||
rejectConversion(new Error("invalid websocket payload"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`received message ${msg.type} for file ${input.name}`);
|
||||
switch (msg.type) {
|
||||
case "progressUpdate": {
|
||||
|
|
@ -781,10 +837,9 @@ export class VertdConverter extends Converter {
|
|||
case "jobFinished": {
|
||||
this.log(`job finished for file ${input.name}`);
|
||||
ws.close();
|
||||
this.activeConversions.delete(input.id);
|
||||
try {
|
||||
const url = `${apiUrl}/api/download/${msg.data.jobId}/${uploadRes.auth}`;
|
||||
this.log(`downloading from ${url}`);
|
||||
// const res = await fetch(url).then((res) => res.blob());
|
||||
const res = await downloadFile(url, input);
|
||||
|
||||
// confirm download to clean up on server
|
||||
|
|
@ -802,24 +857,28 @@ export class VertdConverter extends Converter {
|
|||
this.error(`failed to confirm download: ${e}`);
|
||||
}
|
||||
|
||||
resolve(new VertFile(new File([res], input.name), to));
|
||||
resolveConversion(
|
||||
new VertFile(new File([res], input.name), to),
|
||||
);
|
||||
} catch (e) {
|
||||
if (hash) this.failure(hash);
|
||||
rejectConversion(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "jobCancelled": {
|
||||
this.log("job cancelled");
|
||||
ws.close();
|
||||
this.activeConversions.delete(input.id);
|
||||
reject("Conversion cancelled");
|
||||
rejectConversion("Conversion cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
case "error": {
|
||||
this.error(`error: ${msg.data.message}`);
|
||||
this.activeConversions.delete(input.id);
|
||||
if (hash) this.failure(hash);
|
||||
|
||||
reject({
|
||||
rejectConversion({
|
||||
component: VertdErrorComponent,
|
||||
additional: {
|
||||
jobId: uploadRes.id,
|
||||
|
|
@ -829,6 +888,11 @@ export class VertdConverter extends Converter {
|
|||
errorMessage: msg.data.message,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { PUB_VERTD_URL } from "$env/static/public";
|
||||
import type { ConversionBitrate } from "$lib/converters/ffmpeg.svelte";
|
||||
import type { ConversionSpeed } from "$lib/converters/vertd.svelte";
|
||||
import { readSettings } from "$lib/util/settings";
|
||||
import { VertdInstance } from "./vertdSettings.svelte";
|
||||
|
||||
export { default as Appearance } from "./Appearance.svelte";
|
||||
|
|
@ -62,9 +63,9 @@ export class Settings {
|
|||
public load() {
|
||||
try {
|
||||
VertdInstance.instance.load();
|
||||
const ls = localStorage.getItem("settings");
|
||||
if (!ls) return;
|
||||
const settings: ISettings = JSON.parse(ls);
|
||||
const persisted = readSettings<ISettings>();
|
||||
if (!Object.keys(persisted).length) return;
|
||||
const settings = persisted as ISettings;
|
||||
const vertdBlockedHashes = new Map<string, Date[]>(
|
||||
Object.entries(
|
||||
settings.vertdBlockedHashes ||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { m } from "$lib/paraglide/messages";
|
|||
import sanitizeHtml from "sanitize-html";
|
||||
import { ToastManager } from "$lib/util/toast.svelte";
|
||||
import { GB } from "$lib/util/consts";
|
||||
import { readSettings } from "$lib/util/settings";
|
||||
|
||||
class Files {
|
||||
public files = $state<VertFile[]>([]);
|
||||
|
|
@ -52,9 +53,7 @@ class Files {
|
|||
public requiredConverters = $derived(
|
||||
Array.from(
|
||||
new Set(
|
||||
this.files.flatMap((file) =>
|
||||
this.getRequiredConverters(file),
|
||||
),
|
||||
this.files.flatMap((file) => this.getRequiredConverters(file)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -93,6 +92,9 @@ class Files {
|
|||
?.includes(file.from.toLowerCase());
|
||||
|
||||
try {
|
||||
if (file.blobUrl?.startsWith("blob:"))
|
||||
URL.revokeObjectURL(file.blobUrl);
|
||||
|
||||
if (isAudio) {
|
||||
// try to get the thumbnail from the audio via music-metadata
|
||||
const { common } = await parseBlob(file.file, {
|
||||
|
|
@ -136,8 +138,10 @@ class Files {
|
|||
const mediaElement = isVideo
|
||||
? document.createElement("video")
|
||||
: new Image();
|
||||
mediaElement.src = URL.createObjectURL(file);
|
||||
const mediaUrl = URL.createObjectURL(file);
|
||||
mediaElement.src = mediaUrl;
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
if (isVideo) {
|
||||
const video = mediaElement as HTMLVideoElement;
|
||||
|
|
@ -171,7 +175,12 @@ class Files {
|
|||
ctx.drawImage(mediaElement, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// check if completely transparent
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const imageData = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
);
|
||||
const isTransparent = Array.from(imageData.data).every(
|
||||
(value, index) => {
|
||||
return (index + 1) % 4 !== 0 || value === 0;
|
||||
|
|
@ -185,6 +194,9 @@ class Files {
|
|||
const url = canvas.toDataURL();
|
||||
canvas.remove();
|
||||
return url;
|
||||
} finally {
|
||||
URL.revokeObjectURL(mediaUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleZipFile(file: File): Promise<void> {
|
||||
|
|
@ -435,7 +447,7 @@ class Files {
|
|||
const blob = await downloadZip(dlFiles, "converted.zip").blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem("settings") ?? "{}");
|
||||
const settings = readSettings<{ filenameFormat?: string }>();
|
||||
const filenameFormat = settings.filenameFormat || "VERT_%name%";
|
||||
|
||||
const format = (name: string) => {
|
||||
|
|
@ -603,7 +615,10 @@ export const getMaxArrayBufferSize = (): number => {
|
|||
// lmao uh mobile devices definitely have a much lower limit and using binary search here
|
||||
// was causing crashes especially on iOS, so just return 2GB to be safe :p
|
||||
if (get(isMobile)) {
|
||||
log(["converters"], `mobile device likely detected, using 2GB fallback for max ArrayBuffer size`);
|
||||
log(
|
||||
["converters"],
|
||||
`mobile device likely detected, using 2GB fallback for max ArrayBuffer size`,
|
||||
);
|
||||
// don't save to localStorage, since it can always be a false positive or the user's browser window is simply just small
|
||||
return 2 * GB;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SettingDefinition,
|
||||
} from "./conversion-settings";
|
||||
import { log } from "$lib/util/logger";
|
||||
import { readSettings } from "$lib/util/settings";
|
||||
|
||||
const MAX_BLOB_SIZE_LIMIT = 2 * 1024 * 1024 * 1024; // 2GB
|
||||
|
||||
|
|
@ -121,7 +122,7 @@ export class VertFile {
|
|||
constructor(file: File, to: string, blobUrl?: string) {
|
||||
const ext = file.name.split(".").pop();
|
||||
const newFile = new File(
|
||||
[file.slice(0, file.size, file.type)],
|
||||
[file],
|
||||
`${file.name.split(".").slice(0, -1).join(".")}.${ext?.toLowerCase()}`,
|
||||
);
|
||||
this.file = newFile;
|
||||
|
|
@ -479,7 +480,7 @@ export class VertFile {
|
|||
let to = this.result.to;
|
||||
if (!to.startsWith(".")) to = `.${to}`;
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem("settings") ?? "{}");
|
||||
const settings = readSettings<{ filenameFormat?: string }>();
|
||||
const filenameFormat = settings.filenameFormat || "VERT_%name%";
|
||||
|
||||
const format = (name: string) => {
|
||||
|
|
@ -528,7 +529,7 @@ export class VertFile {
|
|||
|
||||
setTimeout(() => {
|
||||
cache.delete(cacheKey);
|
||||
}, 3000);
|
||||
}, 30000);
|
||||
} else {
|
||||
blob = URL.createObjectURL(
|
||||
new Blob([await this.result.file.arrayBuffer()], {
|
||||
|
|
@ -545,7 +546,9 @@ export class VertFile {
|
|||
a.target = "_blank";
|
||||
a.style.display = "none";
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(blob);
|
||||
}, 30000);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { error } from "$lib/util/logger";
|
||||
|
||||
export function readSettings<T extends object = Record<string, unknown>>(): Partial<T> {
|
||||
if (!browser) return {};
|
||||
|
||||
const raw = localStorage.getItem("settings");
|
||||
if (!raw) return {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
localStorage.removeItem("settings");
|
||||
return {};
|
||||
}
|
||||
|
||||
return parsed as Partial<T>;
|
||||
} catch (e) {
|
||||
error(["settings", "error"], `failed to parse saved settings: ${e}`);
|
||||
localStorage.removeItem("settings");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
@ -46,15 +46,20 @@ class ServiceWorkerManager {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
let settled = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(new Error("Timeout waiting for cache info"));
|
||||
}, 5000);
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
navigator.serviceWorker?.controller?.postMessage(
|
||||
{ type: "GET_CACHE_INFO" },
|
||||
[messageChannel.port2],
|
||||
|
|
@ -69,8 +74,17 @@ class ServiceWorkerManager {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageChannel = new MessageChannel();
|
||||
let settled = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(new Error("Timeout waiting for cache clear"));
|
||||
}, 10000);
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
if (event.data.success) {
|
||||
resolve();
|
||||
} else {
|
||||
|
|
@ -80,10 +94,6 @@ class ServiceWorkerManager {
|
|||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timeout waiting for cache clear"));
|
||||
}, 10000);
|
||||
|
||||
navigator.serviceWorker?.controller?.postMessage(
|
||||
{ type: "CLEAR_CACHE" },
|
||||
[messageChannel.port2],
|
||||
|
|
|
|||
|
|
@ -76,9 +76,16 @@
|
|||
// Check if the data is already in sessionStorage
|
||||
const cachedContribs = sessionStorage.getItem("ghContribs");
|
||||
if (cachedContribs) {
|
||||
ghContribs = JSON.parse(cachedContribs);
|
||||
try {
|
||||
const parsedContribs = JSON.parse(cachedContribs);
|
||||
if (Array.isArray(parsedContribs)) {
|
||||
ghContribs = parsedContribs;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
sessionStorage.removeItem("ghContribs");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch GitHub contributors
|
||||
try {
|
||||
|
|
@ -104,30 +111,16 @@
|
|||
!excludedNames.has(contrib.login),
|
||||
);
|
||||
|
||||
// Fetch and cache avatar images as Base64
|
||||
const fetchAvatar = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
const blob = await res.blob();
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
ghContribs = await Promise.all(
|
||||
filteredContribs.map(
|
||||
async (contrib: {
|
||||
ghContribs = filteredContribs.map(
|
||||
(contrib: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
}) => ({
|
||||
name: contrib.login,
|
||||
avatar: await fetchAvatar(contrib.avatar_url),
|
||||
avatar: contrib.avatar_url,
|
||||
github: contrib.html_url,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Cache the data in sessionStorage
|
||||
|
|
|
|||
|
|
@ -2,17 +2,22 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { error, log } from "$lib/util/logger";
|
||||
import * as Settings from "$lib/sections/settings/index.svelte";
|
||||
import { PUB_PLAUSIBLE_URL } from "$env/static/public";
|
||||
import { SettingsIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { m } from "$lib/paraglide/messages";
|
||||
import { ToastManager } from "$lib/util/toast.svelte";
|
||||
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
|
||||
import { readSettings } from "$lib/util/settings";
|
||||
|
||||
let settings = $state(Settings.Settings.instance.settings);
|
||||
|
||||
let isInitial = $state(true);
|
||||
|
||||
const readSavedSettings = () => {
|
||||
const parsed = readSettings<typeof settings>();
|
||||
return Object.keys(parsed).length ? parsed : null;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
if (isInitial) {
|
||||
|
|
@ -20,12 +25,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const savedSettings = localStorage.getItem("settings");
|
||||
if (savedSettings) {
|
||||
const parsedSettings = JSON.parse(savedSettings);
|
||||
if (JSON.stringify(parsedSettings) === JSON.stringify(settings))
|
||||
const parsedSettings = readSavedSettings();
|
||||
if (parsedSettings && JSON.stringify(parsedSettings) === JSON.stringify(settings))
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Settings.Settings.instance.settings = settings;
|
||||
|
|
@ -41,9 +43,8 @@
|
|||
});
|
||||
|
||||
onMount(() => {
|
||||
const savedSettings = localStorage.getItem("settings");
|
||||
if (savedSettings) {
|
||||
const parsedSettings = JSON.parse(savedSettings);
|
||||
const parsedSettings = readSavedSettings();
|
||||
if (parsedSettings) {
|
||||
Settings.Settings.instance.settings = {
|
||||
...Settings.Settings.instance.settings,
|
||||
...parsedSettings,
|
||||
|
|
|
|||
|
|
@ -126,8 +126,10 @@ self.addEventListener("fetch", (event) => {
|
|||
self.addEventListener("message", (event) => {
|
||||
if (!event.data) return;
|
||||
const type = event.data.type;
|
||||
const port = event.ports?.[0];
|
||||
|
||||
if (type === "GET_CACHE_INFO") {
|
||||
if (!port) return;
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then(async (cache) => {
|
||||
const keys = await cache.keys();
|
||||
|
|
@ -159,7 +161,7 @@ self.addEventListener("message", (event) => {
|
|||
}
|
||||
}
|
||||
|
||||
event.ports[0].postMessage({
|
||||
port.postMessage({
|
||||
totalSize,
|
||||
fileCount: files.length,
|
||||
files,
|
||||
|
|
@ -169,6 +171,7 @@ self.addEventListener("message", (event) => {
|
|||
}
|
||||
|
||||
if (type === "CLEAR_CACHE") {
|
||||
if (!port) return;
|
||||
event.waitUntil(
|
||||
caches
|
||||
.delete(CACHE_NAME)
|
||||
|
|
@ -177,11 +180,11 @@ self.addEventListener("message", (event) => {
|
|||
return caches.open(CACHE_NAME);
|
||||
})
|
||||
.then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
port.postMessage({ success: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[SW] failed to clear cache:", err);
|
||||
event.ports[0].postMessage({
|
||||
port.postMessage({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue