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;
|
||||
}
|
||||
|
||||
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 {
|
||||
@apply border-separator;
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
import { effects, files } from "$lib/store/index.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
jpegify?: boolean;
|
||||
};
|
||||
|
||||
const { class: classList }: Props = $props();
|
||||
const { class: classList, jpegify }: Props = $props();
|
||||
|
||||
let uploaderButton = $state<HTMLButtonElement>();
|
||||
let fileInput = $state<HTMLInputElement>();
|
||||
|
@ -40,10 +42,13 @@
|
|||
|
||||
const handleFileChange = (e: Event) => {
|
||||
if (!fileInput) return;
|
||||
|
||||
const oldLength = files.files.length;
|
||||
files.add(fileInput.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
if (page.url.pathname !== "/jpegify/") {
|
||||
const oldLength = files.files.length;
|
||||
files.add(fileInput.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
} else {
|
||||
files.add(fileInput.files);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
@ -93,7 +98,7 @@
|
|||
<UploadIcon class="w-full h-full text-on-accent" />
|
||||
</div>
|
||||
<h2 class="text-center text-2xl font-semibold mt-4">
|
||||
Drop or click to convert
|
||||
Drop or click to {jpegify ? "JPEGIFY" : "convert"}
|
||||
</h2>
|
||||
</Panel>
|
||||
</button>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
easing: quintOut,
|
||||
}}
|
||||
></div>
|
||||
{:else if page.url.pathname === "/convert/" && $showGradient}
|
||||
{:else if (page.url.pathname === "/convert/" || page.url.pathname === "/jpegify/") && $showGradient}
|
||||
{#key $gradientColor}
|
||||
<div
|
||||
id="gradient-bg"
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
||||
);
|
||||
|
||||
const isSecretPage = $derived(selectedIndex === -1);
|
||||
|
||||
beforeNavigate((e) => {
|
||||
const oldIndex = items.findIndex((i) =>
|
||||
i.activeMatch(e.from?.url.pathname || ""),
|
||||
|
@ -155,16 +157,16 @@
|
|||
|
||||
<div bind:this={container}>
|
||||
<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
|
||||
class="absolute bg-panel-highlight rounded-xl"
|
||||
style="width: {linkRects[selectedIndex]
|
||||
.width}px; height: {linkRects[selectedIndex]
|
||||
.height}px; top: {linkRects[selectedIndex].top -
|
||||
(containerRect?.top || 0)}px; left: {linkRects[
|
||||
selectedIndex
|
||||
].left - (containerRect?.left || 0)}px; {$effects
|
||||
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms;`
|
||||
style="width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -
|
||||
(containerRect?.top || 0)}px; left: {linkRect.left -
|
||||
(containerRect?.left || 0)}px; opacity: {isSecretPage
|
||||
? 0
|
||||
: 1}; {$effects
|
||||
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms, opacity var(--transition) ${duration}ms;`
|
||||
: ''}"
|
||||
></div>
|
||||
{/if}
|
||||
|
|
|
@ -25,6 +25,8 @@ export class Converter {
|
|||
input: VertFile,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
to: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
|
||||
...args: any[]
|
||||
): Promise<VertFile> {
|
||||
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}`);
|
||||
const msg = {
|
||||
type: "convert",
|
||||
|
@ -72,6 +78,7 @@ export class VipsConverter extends Converter {
|
|||
from: input.from,
|
||||
},
|
||||
to,
|
||||
compression,
|
||||
} as WorkerMessage;
|
||||
const res = await this.sendMessage(msg);
|
||||
|
||||
|
|
|
@ -61,7 +61,11 @@
|
|||
<h2 class="text-base font-bold">GitHub contributors</h2>
|
||||
{#if ghContribs && ghContribs.length > 0}
|
||||
<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
|
||||
class="text-blue-500 font-normal hover:underline"
|
||||
href={GITHUB_URL_VERT}
|
||||
|
|
|
@ -4,6 +4,7 @@ interface ConvertMessage {
|
|||
type: "convert";
|
||||
input: VertFile;
|
||||
to: string;
|
||||
compression: number | null;
|
||||
}
|
||||
|
||||
interface FinishedMessage {
|
||||
|
|
|
@ -58,7 +58,8 @@ export class VertFile {
|
|||
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");
|
||||
const converter = this.findConverter();
|
||||
if (!converter) throw new Error("No converter found");
|
||||
|
@ -67,7 +68,7 @@ export class VertFile {
|
|||
this.processing = true;
|
||||
let res;
|
||||
try {
|
||||
res = await converter.convert(this, this.to);
|
||||
res = await converter.convert(this, this.to, ...args);
|
||||
this.result = res;
|
||||
} catch (err) {
|
||||
const castedErr = err as Error;
|
||||
|
|
|
@ -26,7 +26,12 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
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();
|
||||
return {
|
||||
type: "finished",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
} from "$lib/store/index.svelte";
|
||||
import "../app.scss";
|
||||
import { browser } from "$app/environment";
|
||||
import { page } from "$app/state";
|
||||
|
||||
let { children, data } = $props();
|
||||
let enablePlausible = $state(false);
|
||||
|
@ -40,9 +41,13 @@
|
|||
const dropFiles = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dropping.set(false);
|
||||
const oldLength = files.files.length;
|
||||
files.add(e.dataTransfer?.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
if (page.url.pathname !== "/jpegify/") {
|
||||
const oldLength = files.files.length;
|
||||
files.add(e.dataTransfer?.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
} else {
|
||||
files.add(e.dataTransfer?.files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrag = (e: DragEvent, drag: boolean) => {
|
||||
|
|
|
@ -243,7 +243,7 @@
|
|||
? 'bg-accent-green'
|
||||
: 'bg-accent-blue'}"
|
||||
disabled={!files.ready}
|
||||
onclick={file.convert}
|
||||
onclick={() => file.convert()}
|
||||
>
|
||||
<RotateCwIcon size="24" />
|
||||
</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