Compare commits
4 commits
03ede2c023
...
b798a7062a
| Author | SHA1 | Date | |
|---|---|---|---|
| b798a7062a | |||
| 1d24bde610 | |||
| 203e7c4b27 | |||
| 6343bddd18 |
7 changed files with 138 additions and 51 deletions
38
src/lib/blog/auth/CookieAuthentication.ts
Normal file
38
src/lib/blog/auth/CookieAuthentication.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { Cookies } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export class CookieAuthentication {
|
||||||
|
|
||||||
|
private readonly cookieValue: string;
|
||||||
|
private readonly cookieValueArray: string[];
|
||||||
|
public static cookieName = 'auth'
|
||||||
|
public static adminAuthRole = 'admin'
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly cookies: Cookies,
|
||||||
|
){
|
||||||
|
this.cookieValue = this.cookies.get(CookieAuthentication.cookieName) ?? ''
|
||||||
|
this.cookieValueArray = this.cookieValue.split(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isAuthdAsAdmin(): boolean {
|
||||||
|
let isAuthdAsAdmin = false;
|
||||||
|
|
||||||
|
if (this.cookieValueArray.includes(CookieAuthentication.adminAuthRole)) {
|
||||||
|
isAuthdAsAdmin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAuthdAsAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAdminAuthentication(isAuthd: boolean) {
|
||||||
|
let value = this.cookieValue
|
||||||
|
|
||||||
|
if (isAuthd) {
|
||||||
|
value = Array.from(new Set([...this.cookieValueArray, CookieAuthentication.adminAuthRole])).join(',')
|
||||||
|
} else {
|
||||||
|
value = this.cookieValueArray.filter((i) => i !== CookieAuthentication.adminAuthRole).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cookies.set(CookieAuthentication.cookieName, value, { path: '/'})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/routes/admin/+layout.server.ts
Normal file
16
src/routes/admin/+layout.server.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
import type { LayoutServerLoad } from "./$types.js";
|
||||||
|
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = ({ cookies, route }) => {
|
||||||
|
const auth = new CookieAuthentication(cookies)
|
||||||
|
const isAuthd = auth.isAuthdAsAdmin
|
||||||
|
|
||||||
|
if (route.id === '/admin/login' && isAuthd) {
|
||||||
|
return redirect(307, '/admin')
|
||||||
|
} else if (!isAuthd && route.id !== '/admin/login') {
|
||||||
|
return redirect(307, '/admin/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
1
src/routes/admin/+page.svelte
Normal file
1
src/routes/admin/+page.svelte
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>Welcome home, Wilson</h1>
|
||||||
25
src/routes/admin/login/+page.server.ts
Normal file
25
src/routes/admin/login/+page.server.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { PRIVATE_ADMIN_AUTH_TOKEN } from "$env/static/private";
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
import type { Actions} from "./$types.js";
|
||||||
|
import { CookieAuthentication } from "$lib/blog/auth/CookieAuthentication.js";
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({cookies, request}) => {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const token = formData.get('token')
|
||||||
|
|
||||||
|
const isAuthd = PRIVATE_ADMIN_AUTH_TOKEN === token;
|
||||||
|
const auth = new CookieAuthentication(cookies)
|
||||||
|
|
||||||
|
auth.setAdminAuthentication(isAuthd)
|
||||||
|
|
||||||
|
if (isAuthd) {
|
||||||
|
return redirect(307, '/admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} satisfies Actions
|
||||||
11
src/routes/admin/login/+page.svelte
Normal file
11
src/routes/admin/login/+page.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h1>Admin login</h1>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="field">
|
||||||
|
<label for="token">API Token</label>
|
||||||
|
<input type="password" id="token" name="token" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
@ -4,32 +4,47 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const DITHERING_ALGORITHMS = {
|
const DITHERING_ALGORITHMS = {
|
||||||
atkinson: (imageData) => Dither.atkinson(imageData),
|
atkinson: (imageData: ImageData) => Dither.atkinson(imageData),
|
||||||
bayer: (imageData) => Dither.bayer(imageData, 170),
|
bayer: (imageData: ImageData) => Dither.bayer(imageData, 170),
|
||||||
floydSteinberg: (imageData) => Dither.floydsteinberg(imageData),
|
floydSteinberg: (imageData: ImageData) => Dither.floydsteinberg(imageData),
|
||||||
threshold: (imageData) => Dither.threshold(imageData, 2),
|
threshold: (imageData: ImageData) => Dither.threshold(imageData, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let offscreenCanvas: OffscreenCanvas;
|
let offscreenCanvas: OffscreenCanvas;
|
||||||
let ctx = $derived(canvas.getContext('2d'))
|
let ctx = $derived(canvas.getContext('2d'))
|
||||||
let offscreenCtx: OffscreenCanvasRenderingContext2D;
|
let offscreenCtx: OffscreenCanvasRenderingContext2D;
|
||||||
let imgObjectUrl = $state('')
|
let selectedImage: HTMLImageElement | null = $state(null)
|
||||||
let errorMessage = $state('')
|
let errorMessage = $state('')
|
||||||
|
let selectedAlgorithm = $state('atkinson')
|
||||||
|
|
||||||
|
let maxImageWidth = $state(0)
|
||||||
|
let imageHeightRatio = $state(0)
|
||||||
|
let imageWidthPc = $state(100)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
offscreenCanvas = new OffscreenCanvas(1000, 1000)
|
offscreenCanvas = new OffscreenCanvas(1000, 1000)
|
||||||
offscreenCtx = offscreenCanvas.getContext('2d')
|
offscreenCtx = offscreenCanvas.getContext('2d')
|
||||||
})
|
})
|
||||||
|
|
||||||
const doesCtxExist = () => {
|
$effect(() => {
|
||||||
if (!ctx) {
|
if (!selectedImage) {
|
||||||
errorMessage = "Problem finding context for on-screen canvas"
|
console.log(`Selected Image is null, terminating effect...`)
|
||||||
return false
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
maxImageWidth = selectedImage.width
|
||||||
}
|
imageHeightRatio = selectedImage.height / selectedImage.width
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
canvas.width = maxImageWidth * (imageWidthPc/100)
|
||||||
|
canvas.height = imageHeightRatio * canvas.width
|
||||||
|
ctx.drawImage(selectedImage, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
ctx.putImageData(DITHERING_ALGORITHMS[selectedAlgorithm](imageData), 0, 0)
|
||||||
|
})
|
||||||
|
|
||||||
const handleImageChange = async (event: any) => {
|
const handleImageChange = async (event: any) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
@ -38,18 +53,8 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imgObjectUrl = URL.createObjectURL(file);
|
const imgObjectUrl = URL.createObjectURL(file);
|
||||||
const image = await createImage(imgObjectUrl);
|
selectedImage = await createImage(imgObjectUrl);
|
||||||
|
|
||||||
// Change the sizes of canvases
|
|
||||||
canvas.width = image.width
|
|
||||||
canvas.height = image.height
|
|
||||||
|
|
||||||
offscreenCanvas.height = image.height
|
|
||||||
offscreenCanvas.width = image.width
|
|
||||||
|
|
||||||
// Now draw the images
|
|
||||||
drawImage(image)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createImage(objectUrl: string): Promise<HTMLImageElement> {
|
async function createImage(objectUrl: string): Promise<HTMLImageElement> {
|
||||||
|
|
@ -63,30 +68,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearCanvases = () => {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawImage = (theImage: HTMLImageElement) => {
|
|
||||||
if (!doesCtxExist()) return
|
|
||||||
clearCanvases()
|
|
||||||
ctx.drawImage(theImage, 0, 0, canvas.width, canvas.height);
|
|
||||||
offscreenCtx.drawImage(theImage, offscreenCanvas.width, offscreenCanvas.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDitheringAlgorithm(algorithm: (imageData: ImageData) => ImageData) {
|
|
||||||
return async () => {
|
|
||||||
const freshImage = await createImage(imgObjectUrl);
|
|
||||||
drawImage(freshImage)
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
const ditheredImageData = algorithm(imageData);
|
|
||||||
ctx.putImageData(ditheredImageData, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveImage() {
|
function saveImage() {
|
||||||
if (!canvas) return;
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = 'dithered-image.png';
|
link.download = 'dithered-image.png';
|
||||||
link.href = canvas.toDataURL();
|
link.href = canvas.toDataURL();
|
||||||
|
|
@ -95,7 +77,7 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ErrorMessage message={errorMessage} />
|
<ErrorMessage errorMessage={errorMessage} />
|
||||||
|
|
||||||
<h2 class="page-dither__subtitle">1: Select a file</h2>
|
<h2 class="page-dither__subtitle">1: Select a file</h2>
|
||||||
|
|
||||||
|
|
@ -108,9 +90,9 @@
|
||||||
<h2 class="page-dither__subtitle">2: Select a dithering algorithm</h2>
|
<h2 class="page-dither__subtitle">2: Select a dithering algorithm</h2>
|
||||||
|
|
||||||
<ul class="algorithms">
|
<ul class="algorithms">
|
||||||
{#each Object.entries(DITHERING_ALGORITHMS) as [key, value]}
|
{#each Object.keys(DITHERING_ALGORITHMS) as key}
|
||||||
<li class="algorithms__item">
|
<li class="algorithms__item">
|
||||||
<button class="algorithms__button" onclick={applyDitheringAlgorithm(value)}>{key}</button>
|
<button class="algorithms__button" onclick={() => selectedAlgorithm = key}>{key}</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -120,9 +102,20 @@
|
||||||
<canvas id="photo-canvas" height="250" bind:this={canvas} onload={() => ctx = canvas.getContext('2d')}></canvas>
|
<canvas id="photo-canvas" height="250" bind:this={canvas} onload={() => ctx = canvas.getContext('2d')}></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="page-dither__subtitle">
|
||||||
|
3: Remix
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label for="resolution">
|
||||||
|
Image Ratio
|
||||||
|
<input type="range" min="1" max="100" step="5" bind:value={imageWidthPc} width="100%"/>
|
||||||
|
</label>
|
||||||
|
<p>{maxImageWidth * imageWidthPc / 100}</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
<h2 class="page-dither__subtitle">
|
<h2 class="page-dither__subtitle">
|
||||||
3: Save the file
|
4: Save the file
|
||||||
</h2>
|
</h2>
|
||||||
<button onclick={saveImage}>Save Image</button>
|
<button onclick={saveImage}>Save Image</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const { errorMessage } = $props()
|
interface Props {
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
const { errorMessage }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue