Merge branch 'main' into main

This commit is contained in:
nullptr 2025-02-14 20:59:35 +00:00 committed by GitHub
commit 2128445eaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 183 additions and 19 deletions

4
_headers Normal file
View File

@ -0,0 +1,4 @@
# For libvips/wasm-vips converter (images)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

View File

@ -45,6 +45,7 @@
"clsx": "^2.1.1",
"jsmediatags": "^3.9.7",
"lucide-svelte": "^0.475.0",
"vite-plugin-static-copy": "^2.2.0",
"wasm-vips": "^0.0.11"
},
"patchedDependencies": {

View File

@ -2,8 +2,15 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png"">
<link rel="apple-touch-startup-image" href="%sveltekit.assets%/lettermark.jpg">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
%sveltekit.head%
<script>
(function () {
@ -37,6 +44,13 @@
console.log(`Applying theme: ${theme}`);
document.documentElement.classList.add(theme);
// Lock dark reader if it's set to dark mode
if (theme === "dark") {
const lock = document.createElement('meta');
lock.name = 'darkreader-lock';
document.head.appendChild(lock);
}
})();
</script>
</head>

View File

@ -33,6 +33,9 @@ export class VipsConverter extends Converter {
".tif",
".tiff",
".jfif",
//".heif", HEIF files that are encoded like HEIC files (and HEIC files in general) aren't supported due to https://github.com/kleisauke/wasm-vips/issues/3
".avif",
".jxl",
];
public readonly reportsProgress = false;
@ -51,8 +54,6 @@ export class VipsConverter extends Converter {
error(["converters", this.name], `error in worker: ${message.error}`);
addToast("error", `Error in VIPS worker, some features may not work.`);
throw new Error(message.error);
} else {
error(["converters", this.name], `unknown message type: ${message.type}`);
}
};
}

View File

@ -173,7 +173,7 @@ class Files {
const file = files.files[i];
const result = file.result;
if (!result) {
console.error("No result found");
error(["files"], "No result found");
continue;
}
dlFiles.push({
@ -215,6 +215,16 @@ export function setTheme(themeTo: "light" | "dark") {
});
log(["theme"], `set to ${themeTo}`);
theme.set(themeTo);
// Lock dark reader if it's set to dark mode
if (themeTo === "dark") {
const lock = document.createElement('meta');
lock.name = 'darkreader-lock';
document.head.appendChild(lock);
} else {
const lock = document.querySelector('meta[name="darkreader-lock"]');
if (lock) lock.remove();
}
}
export function setEffects(effectsEnabled: boolean) {

View File

@ -1,4 +1,5 @@
import type { Converter } from "$lib/converters/converter.svelte";
import { error } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider";
export class VertFile {
@ -46,7 +47,7 @@ export class VertFile {
res = await this.converter.convert(this, this.to);
this.result = res;
} catch (err) {
console.error(err);
error(["files"], err);
addToast("error", `Error converting file: ${this.file.name}`);
this.result = null;
}

View File

@ -1,10 +1,7 @@
import { type WorkerMessage, type OmitBetterStrict } from "$lib/types";
import Vips from "wasm-vips";
const vipsPromise = Vips({
// see https://github.com/kleisauke/wasm-vips/issues/85
dynamicLibraries: [],
});
const vipsPromise = Vips({});
vipsPromise
.then(() => {
@ -23,6 +20,7 @@ const handleMessage = async (
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
const image = vips.Image.newFromBuffer(
await message.input.file.arrayBuffer(),
`${message.to === ".gif" || message.to === ".webp" ? "[n=-1]" : ""}`,
);
const output = image.writeToBuffer(message.to);
image.delete();

View File

@ -64,7 +64,7 @@
/>
<meta
name="description"
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing is done on your device."
/>
<meta property="og:type" content="website" />
<meta
@ -73,7 +73,7 @@
/>
<meta
property="og:description"
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing is done on your device."
/>
<meta property="og:image" content={featuredImage} />
<meta property="twitter:card" content="summary_large_image" />
@ -83,15 +83,15 @@
/>
<meta
property="twitter:description"
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing is done on your device."
/>
<meta property="twitter:image" content={featuredImage} />
<link rel="manifest" href="/manifest.json" />
{#if PUB_PLAUSIBLE_URL}<script
defer
data-domain={PUB_HOSTNAME || "vert.sh"}
src="{PUB_PLAUSIBLE_URL}/js/script.pageview-props.tagged-events.js"
></script>{/if}
<script src="/coi-serviceworker.min.js"></script>
</svelte:head>
<!-- FIXME: if user resizes between desktop/mobile, highlight of page disappears (only shows on original size) -->

View File

@ -4,7 +4,6 @@
import Uploader from "$lib/components/functional/Uploader.svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import { converters } from "$lib/converters";
import {
effects,
files,
@ -12,6 +11,7 @@
showGradient,
vertdLoaded,
} from "$lib/store/index.svelte";
import { addToast } from "$lib/store/ToastProvider";
import { VertFile } from "$lib/types";
import {
AudioLines,
@ -26,6 +26,18 @@
XIcon,
} from "lucide-svelte";
const handleSelect = (option: string, file: VertFile) => {
file.result = null;
switch (option) {
case ".webp":
case ".gif":
addToast(
"warning",
`Converting this file to "${option}" may take some time if animated.`,
);
}
};
$effect(() => {
// Set gradient color depending on the file types
// TODO: if more file types added, add a "fileType" property to the file object
@ -173,7 +185,7 @@
<Dropdown
options={file.converter?.supportedFormats || []}
bind:selected={file.to}
onselect={() => file.result && (file.result = null)}
onselect={(option) => handleSelect(option, file)}
/>
<div class="w-full flex items-center justify-between">
<button

95
src/service-worker.js Normal file
View File

@ -0,0 +1,95 @@
/// <reference types="@sveltejs/kit" />
// code modified from https://svelte.dev/docs/kit/service-workers
import { build, files, version } from "$service-worker";
// create a unique cache name for this deployment
const CACHE = `cache-${version}`;
const ASSETS = [
...build, // the app itself
...files, // everything in `static`
];
self.addEventListener("install", (event) => {
// create a new cache and add all files to it
async function addFilesToCache() {
try {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
console.log(`assets cached successfully: ${ASSETS}`);
} catch (err) {
console.error(`failed to cache assets: ${err}`);
}
}
console.log(`installing service worker for version ${version}`);
event.waitUntil(addFilesToCache());
});
self.addEventListener("activate", (event) => {
// remove previous cached data from disk
async function deleteOldCaches() {
try {
const keys = await caches.keys();
for (const key of keys) {
if (key !== CACHE) {
await caches.delete(key);
console.log(`deleted old cache: ${key}`);
}
}
} catch (error) {
console.error(`failed to delete old caches: ${error}`);
}
}
event.waitUntil(deleteOldCaches());
});
self.addEventListener("fetch", (event) => {
// ignore requests other than GET
if (event.request.method !== "GET") return;
async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);
// assets can always be served from the cache
if (ASSETS.includes(url.pathname)) {
const response = await cache.match(url.pathname);
if (response) {
console.log(`serving ${url.pathname} from cache`);
return response;
}
}
// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);
// if we're offline, fetch can return a value that is not a Response instead
// of throwing, and we can't pass this non-Response to respondWith
if (!(response instanceof Response)) {
throw new Error("invalid response from fetch");
}
if (response.status === 200)
cache.put(event.request, response.clone());
return response;
} catch (err) {
const response = await cache.match(event.request);
if (response) {
console.log(`Returning ${event.request.url} from cache`);
return response;
}
// if there's no cache, then just error out
// as there is nothing we can do to respond to this request
throw err;
}
}
event.respondWith(respond());
});

View File

@ -1,2 +0,0 @@
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

BIN
static/lettermark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

21
static/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "VERT",
"short_name": "VERT",
"description": "The file converter you'll love",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#F2ABEE",
"icons": [
{
"src": "lettermark.jpg",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "lettermark.jpg",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -1,5 +1,6 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
import svg from "@poppanator/sveltekit-svg";
export default defineConfig({
@ -8,7 +9,7 @@ export default defineConfig({
{
name: "vips-request-middleware",
configureServer(server) {
server.middlewares.use((req, res, next) => {
server.middlewares.use((_req, res, next) => {
res.setHeader(
"Cross-Origin-Embedder-Policy",
"require-corp",
@ -31,6 +32,14 @@ export default defineConfig({
],
},
}),
viteStaticCopy({
targets: [
{
src: "_headers",
dest: "",
},
],
}),
],
optimizeDeps: {
exclude: [