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
+1
View File
@@ -76,6 +76,7 @@
"color": "^4.2.3",
"dompurify": "^3.0.8",
"framer-motion": "^11.0.25",
"idb": "^8.0.0",
"kolorist": "^1.8.0",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
-4
View File
@@ -30,10 +30,6 @@
}
],
"web_accessible_resources": [
{
"resources": ["seqta/ui/background/background.html"],
"matches": ["*://*/*"]
},
{
"resources": ["*://*/*"],
"matches": ["*://*/*"]
+95 -4
View File
@@ -1,13 +1,104 @@
import browser from 'webextension-polyfill';
import { getDataById, isIndexedDBSupported } from '@/svelte-interface/hooks/BackgroundDataLoader';
export async function appendBackgroundToUI() {
const parent = document.getElementById('container');
// embed background.html
const background = document.createElement('iframe');
// embed background.html - old method
/* const background = document.createElement('iframe');
background.id = 'background';
background.classList.add('imageBackground');
background.setAttribute('excludeDarkCheck', 'true');
background.src = browser.runtime.getURL('seqta/ui/background/background.html');
parent!.appendChild(background);
parent!.appendChild(background); */
if (!parent) return;
const backgroundContainer = document.createElement('div');
backgroundContainer.classList.add('imageBackground');
backgroundContainer.setAttribute('excludeDarkCheck', 'true');
const mediaContainer = document.createElement('div');
mediaContainer.id = 'media-container';
backgroundContainer.appendChild(mediaContainer);
parent.appendChild(backgroundContainer);
// Add styles
const style = document.createElement('style');
style.textContent = `
#media-container {
width: 100%;
height: 100%;
}
#media-container video, #media-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
`;
document.head.appendChild(style);
// Load and display the background
await loadBackground();
}
export async function loadBackground() {
if (!isIndexedDBSupported()) {
console.error("IndexedDB is not supported. Unable to load background.");
return;
}
try {
const selectedBackgroundId = localStorage.getItem('selectedBackground');
if (!selectedBackgroundId) {
const backgroundContainer = document.querySelector('.imageBackground');
if (backgroundContainer) {
backgroundContainer.remove();
}
return;
};
const background = await getDataById(selectedBackgroundId);
if (!background) return;
let backgroundContainer = document.querySelector('.imageBackground');
if (!backgroundContainer) {
backgroundContainer = document.createElement('div');
backgroundContainer.classList.add('imageBackground');
backgroundContainer.setAttribute('excludeDarkCheck', 'true');
const parent = document.getElementById('container');
if (parent) {
parent.appendChild(backgroundContainer);
}
}
let mediaContainer = document.getElementById('media-container');
if (!mediaContainer) {
mediaContainer = document.createElement('div');
mediaContainer.id = 'media-container';
backgroundContainer.appendChild(mediaContainer);
};
mediaContainer = document.getElementById('media-container');
if (!mediaContainer) return;
mediaContainer.innerHTML = '';
const mediaElement = background.type === 'video'
? document.createElement('video')
: document.createElement('img');
mediaElement.src = URL.createObjectURL(background.blob);
mediaElement.classList.add('background');
if (mediaElement instanceof HTMLVideoElement) {
mediaElement.loop = true;
mediaElement.muted = true;
mediaElement.autoplay = true;
}
mediaContainer.appendChild(mediaElement);
} catch (error) {
console.error('Error loading background:', error);
}
}
-29
View File
@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Background Fetcher</title>
<style>
body {
margin: 0 !important;
}
video, img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<!-- Container for the media -->
<div id="media-container"></div>
<script type="module" src="./background.ts"></script>
</body>
</html>
-115
View File
@@ -1,115 +0,0 @@
interface Data {
blob: Blob;
type: 'image' | 'video';
}
interface DatabaseEventTarget extends EventTarget {
result: IDBDatabase;
}
interface DatabaseEvent extends Event {
target: DatabaseEventTarget;
}
const openDB = (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
// @ts-expect-error - The event type is not recognized by TypeScript
event?.target?.result.createObjectStore('backgrounds', { keyPath: 'id' });
};
});
};
const readData = async (): Promise<Data | null> => {
const selectedBackground = localStorage.getItem('selectedBackground');
//const selectedBackground = localStorage.getItem('selectedBackground');
if (!selectedBackground || selectedBackground === '') {
return null;
}
const db = await openDB();
const tx = db.transaction('backgrounds', 'readonly');
const store = tx.objectStore('backgrounds');
const request = store.get(selectedBackground);
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result as Data);
request.onerror = () => reject(request.error);
});
};
const updateBackground = async (): Promise<void> => {
try {
const data = await readData();
if (!data) {
const container = document.getElementById('media-container');
const currentMedia = container?.querySelector('.current-media');
if (currentMedia) {
currentMedia.remove();
}
return;
}
const url = URL.createObjectURL(data.blob);
const container = document.getElementById('media-container');
// Create new element and set properties
let newElement;
if (data.type === 'image') {
newElement = document.createElement('img');
newElement.src = url;
newElement.alt = 'Uploaded content';
} else if (data.type === 'video') {
newElement = document.createElement('video');
newElement.src = url;
newElement.autoplay = true;
newElement.loop = true;
newElement.muted = true;
}
// Mark the old element for removal
const oldElement = container?.querySelector('.current-media');
if (oldElement) {
oldElement.classList.remove('current-media');
oldElement.classList.add('old-media');
}
// Add the new element and mark it as current
newElement?.classList.add('current-media');
container?.appendChild(newElement as Node);
// Delay removal of old element
setTimeout(() => {
const oldMedia = container?.querySelector('.old-media');
if (oldMedia) {
oldMedia.remove();
}
}, 100); // 0.1 second delay
} catch (error) {
console.error('An error occurred:', error);
}
};
// Main function to run on page load
const main = async (): Promise<void> => {
await updateBackground();
// Listen for changes to local storage
try {
window.addEventListener('storage', async (event) => {
if (event.key === 'selectedBackground') {
await updateBackground();
}
});
} catch (error) {
console.error('An error occurred:', error);
}
};
main()
@@ -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>
@@ -0,0 +1,75 @@
import { openDB, type IDBPDatabase, type DBSchema } from 'idb';
interface BackgroundDB extends DBSchema {
backgrounds: {
key: string;
value: {
id: string;
type: string;
blob: Blob;
};
};
}
let db: IDBPDatabase<BackgroundDB> | null = null;
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db;
db = await openDB<BackgroundDB>('BackgroundDB', 1, {
upgrade(db: IDBPDatabase<BackgroundDB>) {
db.createObjectStore('backgrounds', { keyPath: 'id' });
},
});
return db;
}
export async function readAllData(): Promise<Array<{ id: string; type: string; blob: Blob }>> {
const db = await openDatabase();
return db.getAll('backgrounds');
}
export async function writeData(id: string, type: string, blob: Blob): Promise<void> {
const db = await openDatabase();
await db.put('backgrounds', { id, type, blob });
}
export async function deleteData(id: string): Promise<void> {
const db = await openDatabase();
await db.delete('backgrounds', id);
}
export async function clearAllData(): Promise<void> {
const db = await openDatabase();
await db.clear('backgrounds');
}
export async function getDataById(id: string): Promise<{ id: string; type: string; blob: Blob } | undefined> {
const db = await openDatabase();
return db.get('backgrounds', id);
}
export function closeDatabase(): void {
if (db) {
db.close();
db = null;
}
}
// Helper function to check if IndexedDB is supported
export function isIndexedDBSupported(): boolean {
return 'indexedDB' in window;
}
// Helper function to check if there's enough storage space
export async function hasEnoughStorageSpace(requiredSpace: number): Promise<boolean> {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const { quota, usage } = await navigator.storage.estimate();
if (quota !== undefined && usage !== undefined) {
return (quota - usage) > requiredSpace;
}
}
// If we can't determine, assume there's enough space
return true;
}
@@ -0,0 +1 @@
export let selectedBackground = $state<string | null>(null);
@@ -1,11 +1,19 @@
<script lang="ts">
import BackgroundSelector from "@/svelte-interface/components/themes/BackgroundSelector.svelte"
// backgrounds
let selectedBackground = $state<string | null>(null);
let selectNoBackground = $state<() => void>(() => { });
let clearTheme = $derived(selectedBackground !== null);
let editMode = $state<boolean>(false);
</script>
<div class="pt-4">
<div class="py-4">
<button
onclick={() => selectNoBackground()}
class="w-full px-4 py-2 mb-4 text-[13px] text-white transition rounded-xl bg-zinc-700/50">
Clear Theme
{ clearTheme ? 'Clear Theme' : 'Select Theme' }
</button>
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
</div>
-1
View File
@@ -1 +0,0 @@
<h1>HI THERE!!!!</h1>
@@ -12,3 +12,5 @@ export function createStandalone() {
setStandalone
};
}
export const standalone = createStandalone();
+5 -5
View File
@@ -7,11 +7,11 @@ export default {
content: [
"./src/**/*.{js,ts,jsx,tsx,html,svelte}",
],
safelist: [
{
pattern: / */,
}
],
//safelist: [
//{
// pattern: / */,
//}
//],
darkMode: "class",
theme: {
fontSize: {
+1 -2
View File
@@ -58,8 +58,7 @@ export default defineConfig({
minify: false,
rollupOptions: {
input: {
settings: join(__dirname, 'src', 'svelte-interface', 'index.html'),
backgrounds: join(__dirname, 'src', 'seqta', 'ui', 'background', 'background.html')
settings: join(__dirname, 'src', 'svelte-interface', 'index.html')
}
}
}