feat(settings): add background selector

This commit is contained in:
sethburkart123
2024-09-08 19:33:47 +10:00
parent f0bdbbb14f
commit c3cb2937c9
16 changed files with 420 additions and 165 deletions
@@ -0,0 +1,36 @@
<script lang="ts">
interface Background {
id: string;
type: string;
blob: Blob;
url?: string;
}
let { bg, isSelected, isEditMode, onClick, onDelete } = $props<{ bg: Background, isSelected: boolean, isEditMode: boolean, onClick: () => void, onDelete: () => void }>();
</script>
<div
onclick={onClick}
onkeydown={onClick}
tabindex="-1"
role="button"
class="relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-2 ring-4' : 'ring-0'}"
>
{#if isEditMode}
<div
tabindex="-1"
role="button"
class="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
onclick={onDelete}
onkeydown={onDelete}
>
<div class="w-4 h-0.5 bg-white"></div>
</div>
{/if}
{#if bg.type === 'image'}
<img class="object-cover w-full h-full rounded-xl" src={bg.url} alt="swatch" />
{:else if bg.type === 'video'}
<video muted loop autoplay src={bg.url} class="object-cover w-full h-full rounded-xl"></video>
{/if}
</div>
@@ -0,0 +1,152 @@
<script lang="ts">
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/svelte-interface/hooks/BackgroundDataLoader'
import BackgroundUploader from './BackgroundUploader.svelte';
import BackgroundItem from './BackgroundItem.svelte'
import { onMount } from 'svelte'
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
let { isEditMode, selectNoBackground = $bindable(), selectedBackground = $bindable() } = $props<{ isEditMode: boolean, selectNoBackground: () => void, selectedBackground: string | null }>();
let backgrounds = $state<{ id: string; type: string; blob: Blob; url?: string }[]>([]);
let isLoading = $state<boolean>(false);
let error = $state<string | null>(null);
let imageBackgrounds = $derived(backgrounds.filter(bg => bg.type === 'image'));
let videoBackgrounds = $derived(backgrounds.filter(bg => bg.type === 'video'));
async function getTheme() {
return localStorage.getItem('selectedBackground');
}
async function setTheme(theme: string) {
localStorage.setItem('selectedBackground', theme);
}
async function handleFileChange(file: File): Promise<void> {
if (!file) return;
try {
if (!isIndexedDBSupported()) {
throw new Error("Your browser doesn't support IndexedDB. Unable to save backgrounds.");
}
const hasSpace = await hasEnoughStorageSpace(file.size);
if (!hasSpace) {
throw new Error("Not enough storage space to save this background.");
}
const fileId = `${Date.now()}-${file.name}`;
const fileType = file.type.split('/')[0];
const blob = new Blob([file], { type: file.type });
await writeData(fileId, fileType, blob);
backgrounds = [...backgrounds, { id: fileId, type: fileType, blob, url: URL.createObjectURL(blob) }];
} catch (e) {
if (e instanceof Error) {
error = e.message;
} else {
error = 'An unknown error occurred';
}
}
}
async function loadBackgrounds(): Promise<void> {
try {
isLoading = true;
error = null;
if (!isIndexedDBSupported()) {
throw new Error("Your browser doesn't support IndexedDB. Unable to load backgrounds.");
}
await openDatabase();
const data = await readAllData();
const dataWithUrls = data.map(bg => ({ ...bg, url: URL.createObjectURL(bg.blob) }));
backgrounds = dataWithUrls;
} catch (e) {
if (e instanceof Error) {
error = e.message;
} else {
error = 'An unknown error occurred';
}
} finally {
isLoading = false;
}
}
function selectBackground(fileId: string): void {
if (selectedBackground === fileId) {
selectNoBackground();
return;
}
selectedBackground = fileId;
setTheme(fileId);
}
async function deleteBackground(fileId: string): Promise<void> {
try {
await deleteData(fileId);
backgrounds = backgrounds.filter(bg => bg.id !== fileId);
if (selectedBackground === fileId) {
selectNoBackground();
}
} catch (e) {
if (e instanceof Error) {
error = `Failed to delete background: ${e.message}`;
} else {
error = 'An unknown error occurred';
}
}
}
selectNoBackground = () => {
selectedBackground = null;
setTheme('');
}
$effect(() => {
loadBackground();
selectedBackground
});
onMount(async () => {
await loadBackgrounds();
selectedBackground = await getTheme();
});
</script>
<div class="relative px-1 py-2">
<h2 class="pb-2 text-lg font-bold">Background Images</h2>
<div class="flex flex-wrap gap-4 mb-4">
{#if !isEditMode}
<BackgroundUploader on:fileChange={e => handleFileChange(e.detail)} />
{/if}
{#each imageBackgrounds as bg (bg.id)}
<BackgroundItem
{bg}
isSelected={selectedBackground === bg.id}
isEditMode={isEditMode}
onClick={() => selectBackground(bg.id)}
onDelete={() => deleteBackground(bg.id)}
/>
{/each}
</div>
<h2 class="py-2 text-lg font-bold">Background Videos</h2>
<div class="flex flex-wrap gap-4">
{#if !isEditMode}
<BackgroundUploader on:fileChange={e => handleFileChange(e.detail)} />
{/if}
{#each videoBackgrounds as bg (bg.id)}
<BackgroundItem
{bg}
isSelected={selectedBackground === bg.id}
isEditMode={isEditMode}
onClick={() => selectBackground(bg.id)}
onDelete={() => deleteBackground(bg.id)}
/>
{/each}
</div>
</div>
@@ -0,0 +1,26 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
dispatch('fileChange', file);
}
}
</script>
<div class="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
<div class="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
<!-- Plus icon -->
</div>
<input
type="file"
accept="image/*, video/*"
on:change={handleFileChange}
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
@@ -0,0 +1,13 @@
<script lang="ts">
let { progress } = $props<{ progress: number }>();
let circumference = $derived(2 * Math.PI * 14);
let offset = $derived(circumference * (1 - (progress / 100)));
</script>
<div class="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full">
<svg class="w-full h-full text-zinc-100 dark:text-zinc-700" viewBox="0 0 36 36">
<circle stroke="currentColor" fill="none" stroke-width="4" stroke-linecap="round" cx="18" cy="18" r="10" stroke-dasharray="{circumference} {circumference}" stroke-dashoffset="0" transform="rotate(-90 18 18)"></circle>
<circle stroke="#3B82F6" fill="none" stroke-width="4" stroke-linecap="round" cx="18" cy="18" r="10" stroke-dasharray="{circumference} {circumference}" stroke-dashoffset="{offset}" transform="rotate(-90 18 18)"></circle>
</svg>
</div>