mirror of https://github.com/VERT-sh/VERT.git
feat: secret jpegifier... shhh...
This commit is contained in:
parent
8afe615f56
commit
45ea828ddf
12
src/app.scss
12
src/app.scss
|
@ -317,6 +317,18 @@ body {
|
||||||
@apply outline outline-accent outline-2;
|
@apply outline outline-accent outline-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
@apply appearance-none bg-panel h-2 rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
@apply appearance-none w-4 h-4 bg-accent rounded-full cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
@apply w-4 h-4 bg-accent rounded-full cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-separator;
|
@apply border-separator;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
import { effects, files } from "$lib/store/index.svelte";
|
import { effects, files } from "$lib/store/index.svelte";
|
||||||
import { converters } from "$lib/converters";
|
import { converters } from "$lib/converters";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
jpegify?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { class: classList }: Props = $props();
|
const { class: classList, jpegify }: Props = $props();
|
||||||
|
|
||||||
let uploaderButton = $state<HTMLButtonElement>();
|
let uploaderButton = $state<HTMLButtonElement>();
|
||||||
let fileInput = $state<HTMLInputElement>();
|
let fileInput = $state<HTMLInputElement>();
|
||||||
|
@ -40,10 +42,13 @@
|
||||||
|
|
||||||
const handleFileChange = (e: Event) => {
|
const handleFileChange = (e: Event) => {
|
||||||
if (!fileInput) return;
|
if (!fileInput) return;
|
||||||
|
if (page.url.pathname !== "/jpegify/") {
|
||||||
const oldLength = files.files.length;
|
const oldLength = files.files.length;
|
||||||
files.add(fileInput.files);
|
files.add(fileInput.files);
|
||||||
if (oldLength !== files.files.length) goto("/convert");
|
if (oldLength !== files.files.length) goto("/convert");
|
||||||
|
} else {
|
||||||
|
files.add(fileInput.files);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -93,7 +98,7 @@
|
||||||
<UploadIcon class="w-full h-full text-on-accent" />
|
<UploadIcon class="w-full h-full text-on-accent" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-center text-2xl font-semibold mt-4">
|
<h2 class="text-center text-2xl font-semibold mt-4">
|
||||||
Drop or click to convert
|
Drop or click to {jpegify ? "JPEGIFY" : "convert"}
|
||||||
</h2>
|
</h2>
|
||||||
</Panel>
|
</Panel>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
easing: quintOut,
|
easing: quintOut,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
{:else if page.url.pathname === "/convert/" && $showGradient}
|
{:else if (page.url.pathname === "/convert/" || page.url.pathname === "/jpegify/") && $showGradient}
|
||||||
{#key $gradientColor}
|
{#key $gradientColor}
|
||||||
<div
|
<div
|
||||||
id="gradient-bg"
|
id="gradient-bg"
|
||||||
|
|
|
@ -72,6 +72,8 @@
|
||||||
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSecretPage = $derived(selectedIndex === -1);
|
||||||
|
|
||||||
beforeNavigate((e) => {
|
beforeNavigate((e) => {
|
||||||
const oldIndex = items.findIndex((i) =>
|
const oldIndex = items.findIndex((i) =>
|
||||||
i.activeMatch(e.from?.url.pathname || ""),
|
i.activeMatch(e.from?.url.pathname || ""),
|
||||||
|
@ -155,16 +157,16 @@
|
||||||
|
|
||||||
<div bind:this={container}>
|
<div bind:this={container}>
|
||||||
<Panel class="max-w-[778px] w-screen h-20 flex items-center gap-3 relative">
|
<Panel class="max-w-[778px] w-screen h-20 flex items-center gap-3 relative">
|
||||||
{#if linkRects[selectedIndex]}
|
{@const linkRect = linkRects.at(selectedIndex) || linkRects[0]}
|
||||||
|
{#if linkRect}
|
||||||
<div
|
<div
|
||||||
class="absolute bg-panel-highlight rounded-xl"
|
class="absolute bg-panel-highlight rounded-xl"
|
||||||
style="width: {linkRects[selectedIndex]
|
style="width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -
|
||||||
.width}px; height: {linkRects[selectedIndex]
|
(containerRect?.top || 0)}px; left: {linkRect.left -
|
||||||
.height}px; top: {linkRects[selectedIndex].top -
|
(containerRect?.left || 0)}px; opacity: {isSecretPage
|
||||||
(containerRect?.top || 0)}px; left: {linkRects[
|
? 0
|
||||||
selectedIndex
|
: 1}; {$effects
|
||||||
].left - (containerRect?.left || 0)}px; {$effects
|
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms, opacity var(--transition) ${duration}ms;`
|
||||||
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms;`
|
|
||||||
: ''}"
|
: ''}"
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -25,6 +25,8 @@ export class Converter {
|
||||||
input: VertFile,
|
input: VertFile,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
to: string,
|
to: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
|
||||||
|
...args: any[]
|
||||||
): Promise<VertFile> {
|
): Promise<VertFile> {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,13 @@ export class VipsConverter extends Converter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async convert(input: VertFile, to: string): Promise<VertFile> {
|
public async convert(
|
||||||
|
input: VertFile,
|
||||||
|
to: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
...args: any[]
|
||||||
|
): Promise<VertFile> {
|
||||||
|
const compression: number | undefined = args.at(0);
|
||||||
log(["converters", this.name], `converting ${input.name} to ${to}`);
|
log(["converters", this.name], `converting ${input.name} to ${to}`);
|
||||||
const msg = {
|
const msg = {
|
||||||
type: "convert",
|
type: "convert",
|
||||||
|
@ -72,6 +78,7 @@ export class VipsConverter extends Converter {
|
||||||
from: input.from,
|
from: input.from,
|
||||||
},
|
},
|
||||||
to,
|
to,
|
||||||
|
compression,
|
||||||
} as WorkerMessage;
|
} as WorkerMessage;
|
||||||
const res = await this.sendMessage(msg);
|
const res = await this.sendMessage(msg);
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,11 @@
|
||||||
<h2 class="text-base font-bold">GitHub contributors</h2>
|
<h2 class="text-base font-bold">GitHub contributors</h2>
|
||||||
{#if ghContribs && ghContribs.length > 0}
|
{#if ghContribs && ghContribs.length > 0}
|
||||||
<p class="text-base text-muted font-normal">
|
<p class="text-base text-muted font-normal">
|
||||||
Big thanks to all these people for helping out!
|
Big <a
|
||||||
|
class="text-black dynadark:text-white"
|
||||||
|
href="/jpegify">thanks</a
|
||||||
|
>
|
||||||
|
to all these people for helping out!
|
||||||
<a
|
<a
|
||||||
class="text-blue-500 font-normal hover:underline"
|
class="text-blue-500 font-normal hover:underline"
|
||||||
href={GITHUB_URL_VERT}
|
href={GITHUB_URL_VERT}
|
||||||
|
|
|
@ -4,6 +4,7 @@ interface ConvertMessage {
|
||||||
type: "convert";
|
type: "convert";
|
||||||
input: VertFile;
|
input: VertFile;
|
||||||
to: string;
|
to: string;
|
||||||
|
compression: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FinishedMessage {
|
interface FinishedMessage {
|
||||||
|
|
|
@ -58,7 +58,8 @@ export class VertFile {
|
||||||
this.blobUrl = blobUrl;
|
this.blobUrl = blobUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async convert() {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public async convert(...args: any[]) {
|
||||||
if (!this.converters.length) throw new Error("No converters found");
|
if (!this.converters.length) throw new Error("No converters found");
|
||||||
const converter = this.findConverter();
|
const converter = this.findConverter();
|
||||||
if (!converter) throw new Error("No converter found");
|
if (!converter) throw new Error("No converter found");
|
||||||
|
@ -67,7 +68,7 @@ export class VertFile {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await converter.convert(this, this.to);
|
res = await converter.convert(this, this.to, ...args);
|
||||||
this.result = res;
|
this.result = res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const castedErr = err as Error;
|
const castedErr = err as Error;
|
||||||
|
|
|
@ -26,7 +26,12 @@ const handleMessage = async (message: any): Promise<any> => {
|
||||||
image = vips.Image.newFromBuffer(buffer, "[n=-1]");
|
image = vips.Image.newFromBuffer(buffer, "[n=-1]");
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = image.writeToBuffer(message.to);
|
const opts: { [key: string]: string } = {};
|
||||||
|
if (typeof message.compression !== "undefined") {
|
||||||
|
opts["Q"] = Math.min(100, message.compression + 1).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = image.writeToBuffer(message.to, opts);
|
||||||
image.delete();
|
image.delete();
|
||||||
return {
|
return {
|
||||||
type: "finished",
|
type: "finished",
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
} from "$lib/store/index.svelte";
|
} from "$lib/store/index.svelte";
|
||||||
import "../app.scss";
|
import "../app.scss";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
|
||||||
let { children, data } = $props();
|
let { children, data } = $props();
|
||||||
let enablePlausible = $state(false);
|
let enablePlausible = $state(false);
|
||||||
|
@ -40,9 +41,13 @@
|
||||||
const dropFiles = (e: DragEvent) => {
|
const dropFiles = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropping.set(false);
|
dropping.set(false);
|
||||||
const oldLength = files.files.length;
|
if (page.url.pathname !== "/jpegify/") {
|
||||||
files.add(e.dataTransfer?.files);
|
const oldLength = files.files.length;
|
||||||
if (oldLength !== files.files.length) goto("/convert");
|
files.add(e.dataTransfer?.files);
|
||||||
|
if (oldLength !== files.files.length) goto("/convert");
|
||||||
|
} else {
|
||||||
|
files.add(e.dataTransfer?.files);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrag = (e: DragEvent, drag: boolean) => {
|
const handleDrag = (e: DragEvent, drag: boolean) => {
|
||||||
|
|
|
@ -243,7 +243,7 @@
|
||||||
? 'bg-accent-green'
|
? 'bg-accent-green'
|
||||||
: 'bg-accent-blue'}"
|
: 'bg-accent-blue'}"
|
||||||
disabled={!files.ready}
|
disabled={!files.ready}
|
||||||
onclick={file.convert}
|
onclick={() => file.convert()}
|
||||||
>
|
>
|
||||||
<RotateCwIcon size="24" />
|
<RotateCwIcon size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { flip } from "$lib/animation";
|
||||||
|
import Uploader from "$lib/components/functional/Uploader.svelte";
|
||||||
|
import Panel from "$lib/components/visual/Panel.svelte";
|
||||||
|
import { files } from "$lib/store/index.svelte";
|
||||||
|
import { quintOut } from "svelte/easing";
|
||||||
|
import { blur } from "svelte/transition";
|
||||||
|
|
||||||
|
const images = $derived(
|
||||||
|
files.files.filter((f) =>
|
||||||
|
f.converters.map((c) => c.name).includes("libvips"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let forcedBlobURLs = $state<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
const jpegify = () => {
|
||||||
|
const imgs = [...images];
|
||||||
|
imgs.map(async (f, i) => {
|
||||||
|
f.to = ".jpeg";
|
||||||
|
const result = await f.convert(compression);
|
||||||
|
if (!result) return;
|
||||||
|
forcedBlobURLs.set(f.id, URL.createObjectURL(result.file));
|
||||||
|
forcedBlobURLs = new Map([...forcedBlobURLs]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let compressionInverted = $state(10);
|
||||||
|
const compression = $derived(100 - compressionInverted);
|
||||||
|
const processing = $derived(images.map((f) => f.processing).includes(true));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
|
||||||
|
<h1 class="text-5xl text-center">SECRET JPEGIFY!!!</h1>
|
||||||
|
<p class="text-muted text-center -mt-4 font-normal italic">
|
||||||
|
(shh... don't tell anyone!)
|
||||||
|
</p>
|
||||||
|
<Uploader class="w-full h-64" jpegify={true} />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
class="w-full h-2 bg-panel rounded-lg appearance-none cursor-pointer"
|
||||||
|
bind:value={compressionInverted}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={jpegify}
|
||||||
|
disabled={processing}
|
||||||
|
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
||||||
|
>JPEGIFY {compressionInverted}%!!!</button
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap flex-row justify-center gap-4">
|
||||||
|
{#each images as file, i (file.id)}
|
||||||
|
<div
|
||||||
|
class="max-w-full w-full h-96"
|
||||||
|
animate:flip={{ duration: 400, easing: quintOut }}
|
||||||
|
transition:blur={{
|
||||||
|
duration: 400,
|
||||||
|
amount: 8,
|
||||||
|
easing: quintOut,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Panel class="w-full h-full flex flex-col gap-4 relative z-0">
|
||||||
|
<div
|
||||||
|
class="relative rounded-xl flex-grow overflow-hidden flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={forcedBlobURLs.get(file.id) ||
|
||||||
|
file.result?.blobUrl ||
|
||||||
|
file.blobUrl}
|
||||||
|
alt={file.name}
|
||||||
|
class="h-full relative"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={forcedBlobURLs.get(file.id) ||
|
||||||
|
file.result?.blobUrl ||
|
||||||
|
file.blobUrl}
|
||||||
|
alt={file.name}
|
||||||
|
class="h-full absolute top-0 left-0 w-full object-cover blur-2xl -z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 flex items-center gap-4 w-full">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
file?.download();
|
||||||
|
}}
|
||||||
|
disabled={!!!file.result}
|
||||||
|
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
URL.revokeObjectURL(
|
||||||
|
forcedBlobURLs.get(file.id) || "",
|
||||||
|
);
|
||||||
|
forcedBlobURLs.delete(file.id);
|
||||||
|
files.files = files.files.filter(
|
||||||
|
(f) => f.id !== file.id,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Reference in New Issue