feat: secret jpegifier... shhh...

This commit is contained in:
not-nullptr 2025-04-14 02:32:33 +01:00
parent 8afe615f56
commit 45ea828ddf
13 changed files with 181 additions and 24 deletions

View File

@ -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;
}

View File

@ -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;
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>

View File

@ -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"

View File

@ -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}

View File

@ -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");
}

View File

@ -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);

View File

@ -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}

View File

@ -4,6 +4,7 @@ interface ConvertMessage {
type: "convert";
input: VertFile;
to: string;
compression: number | null;
}
interface FinishedMessage {

View File

@ -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;

View File

@ -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",

View File

@ -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);
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) => {

View File

@ -243,7 +243,7 @@
? 'bg-accent-green'
: 'bg-accent-blue'}"
disabled={!files.ready}
onclick={file.convert}
onclick={() => file.convert()}
>
<RotateCwIcon size="24" />
</button>

View File

@ -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>