ui: implemented project creation functionality and persistent project storage

This commit is contained in:
corgifist 2025-07-24 02:43:34 +03:00
parent dd3ec99d3c
commit 2e4fe8be22
19 changed files with 831 additions and 257 deletions

97
package-lock.json generated
View File

@ -1,15 +1,17 @@
{
"name": "clipfusion-community",
"name": "clipfusion",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clipfusion-editor",
"name": "clipfusion",
"version": "1.0.0",
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.7",
@ -17,6 +19,7 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-hook/hotkey": "^3.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dexie": "^4.0.11",
@ -27,6 +30,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.60.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"zod": "^4.0.5"
},
@ -1041,6 +1045,34 @@
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz",
"integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@ -1087,6 +1119,36 @@
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
"integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -1658,6 +1720,27 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@react-hook/event": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz",
"integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-hook/hotkey": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@react-hook/hotkey/-/hotkey-3.1.0.tgz",
"integrity": "sha512-ekIIW8S12P/fP9krrsOWZCIqzQPzjz2WVkD3v1epP6SAy/XxDIWgJrItd2ySh0RKLkYcfW8z+eii3W/h1TKjOg==",
"license": "MIT",
"dependencies": {
"@react-hook/event": "^1.2.1"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -6356,6 +6439,16 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/sonner": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@ -1,5 +1,5 @@
{
"name": "clipfusion-community",
"name": "clipfusion",
"version": "1.0.0",
"private": true,
"scripts": {
@ -10,7 +10,9 @@
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.7",
@ -18,6 +20,7 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-hook/hotkey": "^3.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dexie": "^4.0.11",
@ -28,6 +31,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.60.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"zod": "^4.0.5"
},

View File

@ -1,7 +1,3 @@
ARG ENABLE_ANALYTICS=false
ARG ANALYTICS_SCRIPT
ARG ANALYTICS_WEBSITE_ID
FROM node:lts-alpine3.22 AS base
FROM base AS deps

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="512" height="512" viewBox="0 0 512 512">
<defs>
</defs>
<rect x="0.0" y="0.0" width="51.2" height="51.2" fill="#ff0040" />
<rect x="0.0" y="51.2" width="51.2" height="51.2" fill="#ff1c38" />
<rect x="0.0" y="102.4" width="51.2" height="51.2" fill="#ff3831" />
<rect x="0.0" y="153.60000000000002" width="51.2" height="51.2" fill="#ff552a" />
<rect x="0.0" y="204.8" width="51.2" height="51.2" fill="#ff7123" />
<rect x="0.0" y="256.0" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="0.0" y="307.20000000000005" width="51.2" height="51.2" fill="#ffaa15" />
<rect x="0.0" y="358.40000000000003" width="51.2" height="51.2" fill="#ffc60e" />
<rect x="0.0" y="409.6" width="51.2" height="51.2" fill="#ffe207" />
<rect x="0.0" y="460.8" width="51.2" height="51.2" fill="#ffff00" />
<rect x="51.2" y="0.0" width="51.2" height="51.2" fill="#ff1c38" />
<rect x="51.2" y="51.2" width="51.2" height="51.2" fill="#ff3233" />
<rect x="51.2" y="102.4" width="51.2" height="51.2" fill="#ff482d" />
<rect x="51.2" y="153.60000000000002" width="51.2" height="51.2" fill="#ff5e28" />
<rect x="51.2" y="204.8" width="51.2" height="51.2" fill="#ff7422" />
<rect x="51.2" y="256.0" width="51.2" height="51.2" fill="#ff8a1d" />
<rect x="51.2" y="307.20000000000005" width="51.2" height="51.2" fill="#ffa017" />
<rect x="51.2" y="358.40000000000003" width="51.2" height="51.2" fill="#ffb612" />
<rect x="51.2" y="409.6" width="51.2" height="51.2" fill="#ffcc0c" />
<rect x="51.2" y="460.8" width="51.2" height="51.2" fill="#ffe207" />
<rect x="102.4" y="0.0" width="51.2" height="51.2" fill="#ff3831" />
<rect x="102.4" y="51.2" width="51.2" height="51.2" fill="#ff482d" />
<rect x="102.4" y="102.4" width="51.2" height="51.2" fill="#ff5829" />
<rect x="102.4" y="153.60000000000002" width="51.2" height="51.2" fill="#ff6725" />
<rect x="102.4" y="204.8" width="51.2" height="51.2" fill="#ff7721" />
<rect x="102.4" y="256.0" width="51.2" height="51.2" fill="#ff871e" />
<rect x="102.4" y="307.20000000000005" width="51.2" height="51.2" fill="#ff971a" />
<rect x="102.4" y="358.40000000000003" width="51.2" height="51.2" fill="#ffa616" />
<rect x="102.4" y="409.6" width="51.2" height="51.2" fill="#ffb612" />
<rect x="102.4" y="460.8" width="51.2" height="51.2" fill="#ffc60e" />
<rect x="153.60000000000002" y="0.0" width="51.2" height="51.2" fill="#ff552a" />
<rect x="153.60000000000002" y="51.2" width="51.2" height="51.2" fill="#ff5e28" />
<rect x="153.60000000000002" y="102.4" width="51.2" height="51.2" fill="#ff6725" />
<rect x="153.60000000000002" y="153.60000000000002" width="51.2" height="51.2" fill="#ff7123" />
<rect x="153.60000000000002" y="204.8" width="51.2" height="51.2" fill="#ff7a21" />
<rect x="153.60000000000002" y="256.0" width="51.2" height="51.2" fill="#ff841e" />
<rect x="153.60000000000002" y="307.20000000000005" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="153.60000000000002" y="358.40000000000003" width="51.2" height="51.2" fill="#ff971a" />
<rect x="153.60000000000002" y="409.6" width="51.2" height="51.2" fill="#ffa017" />
<rect x="153.60000000000002" y="460.8" width="51.2" height="51.2" fill="#ffaa15" />
<rect x="204.8" y="0.0" width="51.2" height="51.2" fill="#ff7123" />
<rect x="204.8" y="51.2" width="51.2" height="51.2" fill="#ff7422" />
<rect x="204.8" y="102.4" width="51.2" height="51.2" fill="#ff7721" />
<rect x="204.8" y="153.60000000000002" width="51.2" height="51.2" fill="#ff7a21" />
<rect x="204.8" y="204.8" width="51.2" height="51.2" fill="#ff7d20" />
<rect x="204.8" y="256.0" width="51.2" height="51.2" fill="#ff811f" />
<rect x="204.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ff841e" />
<rect x="204.8" y="358.40000000000003" width="51.2" height="51.2" fill="#ff871e" />
<rect x="204.8" y="409.6" width="51.2" height="51.2" fill="#ff8a1d" />
<rect x="204.8" y="460.8" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="256.0" y="0.0" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="256.0" y="51.2" width="51.2" height="51.2" fill="#ff8a1d" />
<rect x="256.0" y="102.4" width="51.2" height="51.2" fill="#ff871e" />
<rect x="256.0" y="153.60000000000002" width="51.2" height="51.2" fill="#ff841e" />
<rect x="256.0" y="204.8" width="51.2" height="51.2" fill="#ff811f" />
<rect x="256.0" y="256.0" width="51.2" height="51.2" fill="#ff7d20" />
<rect x="256.0" y="307.20000000000005" width="51.2" height="51.2" fill="#ff7a21" />
<rect x="256.0" y="358.40000000000003" width="51.2" height="51.2" fill="#ff7721" />
<rect x="256.0" y="409.6" width="51.2" height="51.2" fill="#ff7422" />
<rect x="256.0" y="460.8" width="51.2" height="51.2" fill="#ff7123" />
<rect x="307.20000000000005" y="0.0" width="51.2" height="51.2" fill="#ffaa15" />
<rect x="307.20000000000005" y="51.2" width="51.2" height="51.2" fill="#ffa017" />
<rect x="307.20000000000005" y="102.4" width="51.2" height="51.2" fill="#ff971a" />
<rect x="307.20000000000005" y="153.60000000000002" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="307.20000000000005" y="204.8" width="51.2" height="51.2" fill="#ff841e" />
<rect x="307.20000000000005" y="256.0" width="51.2" height="51.2" fill="#ff7a21" />
<rect x="307.20000000000005" y="307.20000000000005" width="51.2" height="51.2" fill="#ff7123" />
<rect x="307.20000000000005" y="358.40000000000003" width="51.2" height="51.2" fill="#ff6725" />
<rect x="307.20000000000005" y="409.6" width="51.2" height="51.2" fill="#ff5e28" />
<rect x="307.20000000000005" y="460.8" width="51.2" height="51.2" fill="#ff552a" />
<rect x="358.40000000000003" y="0.0" width="51.2" height="51.2" fill="#ffc60e" />
<rect x="358.40000000000003" y="51.2" width="51.2" height="51.2" fill="#ffb612" />
<rect x="358.40000000000003" y="102.4" width="51.2" height="51.2" fill="#ffa616" />
<rect x="358.40000000000003" y="153.60000000000002" width="51.2" height="51.2" fill="#ff971a" />
<rect x="358.40000000000003" y="204.8" width="51.2" height="51.2" fill="#ff871e" />
<rect x="358.40000000000003" y="256.0" width="51.2" height="51.2" fill="#ff7721" />
<rect x="358.40000000000003" y="307.20000000000005" width="51.2" height="51.2" fill="#ff6725" />
<rect x="358.40000000000003" y="358.40000000000003" width="51.2" height="51.2" fill="#ff5829" />
<rect x="358.40000000000003" y="409.6" width="51.2" height="51.2" fill="#ff482d" />
<rect x="358.40000000000003" y="460.8" width="51.2" height="51.2" fill="#ff3831" />
<rect x="409.6" y="0.0" width="51.2" height="51.2" fill="#ffe207" />
<rect x="409.6" y="51.2" width="51.2" height="51.2" fill="#ffcc0c" />
<rect x="409.6" y="102.4" width="51.2" height="51.2" fill="#ffb612" />
<rect x="409.6" y="153.60000000000002" width="51.2" height="51.2" fill="#ffa017" />
<rect x="409.6" y="204.8" width="51.2" height="51.2" fill="#ff8a1d" />
<rect x="409.6" y="256.0" width="51.2" height="51.2" fill="#ff7422" />
<rect x="409.6" y="307.20000000000005" width="51.2" height="51.2" fill="#ff5e28" />
<rect x="409.6" y="358.40000000000003" width="51.2" height="51.2" fill="#ff482d" />
<rect x="409.6" y="409.6" width="51.2" height="51.2" fill="#ff3233" />
<rect x="409.6" y="460.8" width="51.2" height="51.2" fill="#ff1c38" />
<rect x="460.8" y="0.0" width="51.2" height="51.2" fill="#ffff00" />
<rect x="460.8" y="51.2" width="51.2" height="51.2" fill="#ffe207" />
<rect x="460.8" y="102.4" width="51.2" height="51.2" fill="#ffc60e" />
<rect x="460.8" y="153.60000000000002" width="51.2" height="51.2" fill="#ffaa15" />
<rect x="460.8" y="204.8" width="51.2" height="51.2" fill="#ff8d1c" />
<rect x="460.8" y="256.0" width="51.2" height="51.2" fill="#ff7123" />
<rect x="460.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ff552a" />
<rect x="460.8" y="358.40000000000003" width="51.2" height="51.2" fill="#ff3831" />
<rect x="460.8" y="409.6" width="51.2" height="51.2" fill="#ff1c38" />
<rect x="460.8" y="460.8" width="51.2" height="51.2" fill="#ff0040" />
</svg>

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -6,6 +6,8 @@ import Dashboard from "@/components/dashboard";
import "./globals.css";
import ThemeProvider from "./theme-provider";
import Analytics from "./analytics";
import PersistenceProvider from "./persistence-provider";
import { Toaster } from "@/components/ui/sonner";
const geist = Geist({
variable: "--font-geist",
@ -23,7 +25,7 @@ export const metadata: Metadata = {
};
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: ReactNode;
@ -37,10 +39,13 @@ export default function RootLayout({
<ThemeProvider>
<SidebarProvider>
<Dashboard/>
<main className="w-full">
{children}
<main className="w-full h-full">
<PersistenceProvider>
{children}
</PersistenceProvider>
</main>
</SidebarProvider>
<Toaster/>
</ThemeProvider>
</body>
</html>

View File

@ -1,23 +1,41 @@
"use client";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { ReactNode } from "react";
import { FormEvent, ReactNode, useState } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "@/lib/db";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { ListIcon, PlusIcon } from "lucide-react";
import { ListCheckIcon, PlusIcon } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
import Search from "@/components/search";
import { useIsMobile } from "@/hooks/use-mobile";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Card } from "@/components/ui/card";
import Project from "@/types/Project";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { generateUUID } from "@/lib/uuid";
const Project = (): ReactNode => {
const NewProjectFormSchema = z.object({
title: z.string().nonempty("Title cannot be empty"),
description: z.string().or(z.literal(""))
});
const ProjectContainer = ({
project
}: {
project: Project
}): ReactNode => {
return (
<AspectRatio ratio={16 / 9}>
<AspectRatio ratio={16 / 9} key={project.uuid}>
<Card className=" rounded-lg shadow-md p-4 w-full h-full overflow-hidden">
<h3 className="text-lg font-semibold">Project Title</h3>
<p className="text-sm text-gray-600">Project description goes here.</p>
<h3 className="text-lg font-semibold">{project.title}</h3>
{project.description && <p className="text-sm text-gray-600">{project.description}</p>}
</Card>
</AspectRatio>
)
@ -25,39 +43,104 @@ const Project = (): ReactNode => {
export default function Home(): ReactNode {
const isMobile = useIsMobile();
const [search, setSearch] = useState('');
const projectsCount = useLiveQuery(() => {
return db.projects.count();
const projects = useLiveQuery(() => {
return db.projects.filter((project) => project.title.includes(search)).toArray();
});
const newProjectForm = useForm<z.infer<typeof NewProjectFormSchema>>({
resolver: zodResolver(NewProjectFormSchema),
defaultValues: {
title: "New ClipFusion Project",
description: ""
}
});
const newProjectSubmit = async (data: z.infer<typeof NewProjectFormSchema>) => {
const date = Date.now();
await db.projects.add({
uuid: generateUUID(),
creationDate: date,
editDate: date,
title: data.title,
description: data.description,
origin: ""
});
};
return (
<div className="p-5 w-full">
<div className="p-5 w-full h-full">
<div className="flex flex-row items-center gap-2">
<SidebarTrigger size="lg"/>
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Project Library</h2>
<Label className="text-muted-foreground text-sm">(Found {projectsCount != undefined ? projectsCount : '-'} projects)</Label>
{projects && <Label className="text-muted-foreground text-sm">(Found {projects.length} projects)</Label>}
</div>
<div className="flex flex-row items-center justify-between sticky top-0 bg-background gap-2 mt-3 pb-2 pt-2 w-full overscroll-none z-50">
<div className="flex flex-row items-center gap-2">
<Button>
<PlusIcon/> {!isMobile && "New Project"}
</Button>
<Dialog>
<DialogTrigger asChild>
<Button>
<PlusIcon/> {!isMobile && "New Project"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Create New Project
</DialogTitle>
<DialogDescription>
Fill in the information about your project. You can change it at any time later.
</DialogDescription>
</DialogHeader>
<Form {...newProjectForm}>
<form onSubmit={newProjectForm.handleSubmit(newProjectSubmit)} className="grid gap-3">
<FormField control={newProjectForm.control} name="title" render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field}/>
</FormControl>
<FormMessage/>
</FormItem>
)}/>
<FormField control={newProjectForm.control} name="description" render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Tell something about your project" className="resize-y" {...field}/>
</FormControl>
<FormMessage/>
</FormItem>
)}/>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button type="submit">Create</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
<div className="flex flex-row items-center gap-2">
<Toggle variant="outline">
<ListIcon/> {!isMobile && "Select Projects"}
<ListCheckIcon/> {!isMobile && "Select Projects"}
</Toggle>
<Search placeholder="Search Projects"/>
<Search placeholder="Search Projects" value={search} onChange={(e) => setSearch(e.target.value)}/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-5">
<Project/>
<Project/>
<Project/>
<Project/>
<Project/>
<Project/>
{projects && projects.map((project) => <ProjectContainer project={project}/>)}
</div>
{(projects != undefined && projects.length == 0) && (
<div className="w-full h-full flex justify-center items-center">
<Label className="text-muted-foreground">Nothing to Show</Label>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,106 @@
"use client";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { isStoragePersisted, persist, tryPersistWithoutPromtingUser } from "@/lib/db";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import { toast } from "sonner";
interface PersistenceContextData {
persist: () => void;
}
const PersistenceContext = createContext<PersistenceContextData | null>(null);
export const usePersistenceContext = (): PersistenceContextData => {
const context = useContext(PersistenceContext);
if (context == null) throw new Error("PersistenceContext is not provided!");
return context;
}
const PersistenceProvider = ({
children
}: {
children: ReactNode
}): ReactNode => {
const [persistenceAlertOpen, setPersistenceAlertOpen] = useState(false);
const [requestPersistenceAlertOpen, setRequestPersistenceOpen] = useState(false);
useEffect(() => {
const tryToPersist = async () => {
const isPersistent = await isStoragePersisted();
console.log(isPersistent);
if (!isPersistent) {
if (localStorage.getItem('persistence-status') != "persisted" && localStorage.getItem('persistence-status') == undefined) {
const persistenceStatus = await tryPersistWithoutPromtingUser();
localStorage.setItem("persistence-status", persistenceStatus);
if (persistenceStatus == "never") {
setPersistenceAlertOpen(true);
}
if (persistenceStatus == "prompt") {
setRequestPersistenceOpen(true);
}
}
}
};
tryToPersist();
}, []);
const allowPersistenceStorage = async () => {
const persistResult = await persist();
if (!persistResult) {
console.log("Something went really wrong while enabling persistent storage");
toast("Failed to Enable Persistent Storage", {
description: persistResult == undefined ? "Persistent storage is not supported in your browser" : "Try again later"
})
return;
}
localStorage.setItem('persistence-status', "persisted");
};
return (
<>
<PersistenceContext.Provider value={{persist: () => setRequestPersistenceOpen(true)}}>
{children}
</PersistenceContext.Provider>
<AlertDialog open={persistenceAlertOpen} onOpenChange={setPersistenceAlertOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Persistent Storage is Unavailable
</AlertDialogTitle>
<AlertDialogDescription>
The browser may delete your local projects without notifying you in case it needs to free up space for other website's data that was used more recently than ClipFusion.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction>
OK
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={requestPersistenceAlertOpen} onOpenChange={setRequestPersistenceOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Enable Persistent Storage
</AlertDialogTitle>
<AlertDialogDescription>
Persistent storage prevents browser from deleting your local data to free up space for other websites. You can enable persistent storage later in settings
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={allowPersistenceStorage}>
Enable
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
export default PersistenceProvider;

View File

@ -1,13 +1,58 @@
"use client";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { ReactNode } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { CheckedState } from "@radix-ui/react-checkbox";
import { InfoIcon } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
import { usePersistenceContext } from "../persistence-provider";
function PersistentStorageControl({
status
}: {
status: string | null
}): ReactNode {
const { persist } = usePersistenceContext();
if (status == null || status == '') return <Label className="text-muted-foreground">No information</Label>;
if (status == "never") return <Label className="text-red-500">Unavailable</Label>;
if (status == "prompt") return <Button onClick={persist}>Enable</Button>;
return <Label className="text-green-400">Enabled</Label>;
};
export default function Settings(): ReactNode {
const [status, setStatus] = useState<string | null>('');
useEffect(() => {
setStatus(localStorage.getItem("persistence-status"));
}, []);
return (
<div className="p-5">
<div className="p-5 w-full">
<div className="flex flex-row items-center gap-2">
<SidebarTrigger/>
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Settings</h2>
</div>
<div className="flex flex-col gap-1 md:lg:gap-2 mt-2 md:mt-4 lg:mt-5">
<h3 className="font-semibold break-keep text-lg sm:text-xl md:text-2xl lg:text-3xl leading-none">Storage</h3>
<div className="">
<div className="flex flex-row justify-between items-center w-full max-w-96">
<div className="flex flex-row gap-2 items-center">
<Label>Persistent Storage</Label>
<Tooltip>
<TooltipTrigger>
<InfoIcon size="15" className="opacity-60 hover:opacity-80"/>
</TooltipTrigger>
<TooltipContent>
Persistent storage prevents browser from deleting your local data to free up space for other websites.
</TooltipContent>
</Tooltip>
</div>
<PersistentStorageControl status={status}/>
</div>
</div>
</div>
</div>
);
}

View File

@ -34,9 +34,11 @@ export const Dashboard = (): ReactNode => {
return (
<Sidebar>
<SidebarHeader className="flex justify-center items-center mt-2">
<ClipFusionLogo width="30" height="30">
<p className="font-bold text-xl select-none">ClipFusion</p>
</ClipFusionLogo>
<Link href="/">
<ClipFusionLogo width="30" height="30">
<p className="font-bold text-xl select-none">ClipFusion</p>
</ClipFusionLogo>
</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
@ -78,7 +80,7 @@ export const Dashboard = (): ReactNode => {
<PlusIcon/> <span className="sr-only">Add Folder</span>
</SidebarGroupAction>
<SidebarGroupContent>
<Label className="flex justify-center text-sm text-muted-foreground">Nothing to show</Label>
<Label className="flex justify-center text-sm text-muted-foreground">Nothing to Show</Label>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
@ -92,7 +94,7 @@ export const Dashboard = (): ReactNode => {
<Tooltip>
<TooltipTrigger>
<a href="https://github.com/ClipFusion-org/clipfusion" target="_blank">
<Image src="/github-mark.svg" width="25" height="25" alt="ClipFusion GitHub Repository" className="dark:invert hover:opacity-95 active:scale-95"/>
<Image src="/github-mark.svg" width="25" height="25" alt="ClipFusion GitHub Repository" className="duration-100 dark:invert hover:opacity-95 active:scale-95"/>
</a>
</TooltipTrigger>
<TooltipContent>
@ -102,7 +104,7 @@ export const Dashboard = (): ReactNode => {
<Tooltip>
<TooltipTrigger>
<a href="https://git.clipfusion.org/ClipFusion-org/clipfusion" target="_blank">
<Image src="/clipfusion-git-logo.png" width="25" height="25" alt="ClipFusion Git Mirror" className="grayscale hover:opacity-95 active:scale-95"/>
<Image src="/clipfusion-git-logo.png" width="25" height="25" alt="ClipFusion Git Mirror" className="duration-100 hover:opacity-95 active:scale-95"/>
</a>
</TooltipTrigger>
<TooltipContent>

View File

@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="512" height="512" viewBox="0 0 512 512">
<defs>
</defs>
<rect x="0.0" y="0.0" width="51.2" height="51.2" fill="#e77728" />
<rect x="0.0" y="51.2" width="51.2" height="51.2" fill="#e78523" />
<rect x="0.0" y="102.4" width="51.2" height="51.2" fill="#e9931f" />
<rect x="0.0" y="153.60000000000002" width="51.2" height="51.2" fill="#eaa11a" />
<rect x="0.0" y="204.8" width="51.2" height="51.2" fill="#ebaf16" />
<rect x="0.0" y="256.0" width="51.2" height="51.2" fill="#ecbd11" />
<rect x="0.0" y="307.20000000000005" width="51.2" height="51.2" fill="#edcb0d" />
<rect x="0.0" y="358.40000000000003" width="51.2" height="51.2" fill="#eed908" />
<rect x="0.0" y="409.6" width="51.2" height="51.2" fill="#efe704" />
<rect x="0.0" y="460.8" width="51.2" height="51.2" fill="#f0f600" />
<rect x="51.2" y="0.0" width="51.2" height="51.2" fill="#cd7c36" />
<rect x="51.2" y="51.2" width="51.2" height="51.2" fill="#d18732" />
<rect x="51.2" y="102.4" width="51.2" height="51.2" fill="#d5922f" />
<rect x="51.2" y="153.60000000000002" width="51.2" height="51.2" fill="#d99e2b" />
<rect x="51.2" y="204.8" width="51.2" height="51.2" fill="#dda928" />
<rect x="51.2" y="256.0" width="51.2" height="51.2" fill="#e1b424" />
<rect x="51.2" y="307.20000000000005" width="51.2" height="51.2" fill="#e5bf20" />
<rect x="51.2" y="358.40000000000003" width="51.2" height="51.2" fill="#e9ca1d" />
<rect x="51.2" y="409.6" width="51.2" height="51.2" fill="#edd619" />
<rect x="51.2" y="460.8" width="51.2" height="51.2" fill="#f1e116" />
<rect x="102.4" y="0.0" width="51.2" height="51.2" fill="#b48144" />
<rect x="102.4" y="51.2" width="51.2" height="51.2" fill="#bb8a42" />
<rect x="102.4" y="102.4" width="51.2" height="51.2" fill="#c2923f" />
<rect x="102.4" y="153.60000000000002" width="51.2" height="51.2" fill="#c99a3c" />
<rect x="102.4" y="204.8" width="51.2" height="51.2" fill="#d0a339" />
<rect x="102.4" y="256.0" width="51.2" height="51.2" fill="#d7ab37" />
<rect x="102.4" y="307.20000000000005" width="51.2" height="51.2" fill="#deb334" />
<rect x="102.4" y="358.40000000000003" width="51.2" height="51.2" fill="#e5bc31" />
<rect x="102.4" y="409.6" width="51.2" height="51.2" fill="#ecc42e" />
<rect x="102.4" y="460.8" width="51.2" height="51.2" fill="#f3cc2c" />
<rect x="153.60000000000002" y="0.0" width="51.2" height="51.2" fill="#9b8753" />
<rect x="153.60000000000002" y="51.2" width="51.2" height="51.2" fill="#a58c51" />
<rect x="153.60000000000002" y="102.4" width="51.2" height="51.2" fill="#af924f" />
<rect x="153.60000000000002" y="153.60000000000002" width="51.2" height="51.2" fill="#b9974d" />
<rect x="153.60000000000002" y="204.8" width="51.2" height="51.2" fill="#c39c4b" />
<rect x="153.60000000000002" y="256.0" width="51.2" height="51.2" fill="#cda249" />
<rect x="153.60000000000002" y="307.20000000000005" width="51.2" height="51.2" fill="#d7a748" />
<rect x="153.60000000000002" y="358.40000000000003" width="51.2" height="51.2" fill="#e1ad46" />
<rect x="153.60000000000002" y="409.6" width="51.2" height="51.2" fill="#ebb244" />
<rect x="153.60000000000002" y="460.8" width="51.2" height="51.2" fill="#f5b842" />
<rect x="204.8" y="0.0" width="51.2" height="51.2" fill="#828c61" />
<rect x="204.8" y="51.2" width="51.2" height="51.2" fill="#8f8f60" />
<rect x="204.8" y="102.4" width="51.2" height="51.2" fill="#9c915f" />
<rect x="204.8" y="153.60000000000002" width="51.2" height="51.2" fill="#a9945e" />
<rect x="204.8" y="204.8" width="51.2" height="51.2" fill="#b6965d" />
<rect x="204.8" y="256.0" width="51.2" height="51.2" fill="#c3995c" />
<rect x="204.8" y="307.20000000000005" width="51.2" height="51.2" fill="#cf9b5b" />
<rect x="204.8" y="358.40000000000003" width="51.2" height="51.2" fill="#dc9e5a" />
<rect x="204.8" y="409.6" width="51.2" height="51.2" fill="#e9a059" />
<rect x="204.8" y="460.8" width="51.2" height="51.2" fill="#f6a358" />
<rect x="256.0" y="0.0" width="51.2" height="51.2" fill="#699270" />
<rect x="256.0" y="51.2" width="51.2" height="51.2" fill="#799170" />
<rect x="256.0" y="102.4" width="51.2" height="51.2" fill="#89916f" />
<rect x="256.0" y="153.60000000000002" width="51.2" height="51.2" fill="#99916f" />
<rect x="256.0" y="204.8" width="51.2" height="51.2" fill="#a8906f" />
<rect x="256.0" y="256.0" width="51.2" height="51.2" fill="#b8906f" />
<rect x="256.0" y="307.20000000000005" width="51.2" height="51.2" fill="#c88f6f" />
<rect x="256.0" y="358.40000000000003" width="51.2" height="51.2" fill="#d88f6e" />
<rect x="256.0" y="409.6" width="51.2" height="51.2" fill="#e88f6e" />
<rect x="256.0" y="460.8" width="51.2" height="51.2" fill="#f88e6e" />
<rect x="307.20000000000005" y="0.0" width="51.2" height="51.2" fill="#50977e" />
<rect x="307.20000000000005" y="51.2" width="51.2" height="51.2" fill="#63947f" />
<rect x="307.20000000000005" y="102.4" width="51.2" height="51.2" fill="#769180" />
<rect x="307.20000000000005" y="153.60000000000002" width="51.2" height="51.2" fill="#888d80" />
<rect x="307.20000000000005" y="204.8" width="51.2" height="51.2" fill="#9b8a81" />
<rect x="307.20000000000005" y="256.0" width="51.2" height="51.2" fill="#ae8782" />
<rect x="307.20000000000005" y="307.20000000000005" width="51.2" height="51.2" fill="#c18382" />
<rect x="307.20000000000005" y="358.40000000000003" width="51.2" height="51.2" fill="#d48083" />
<rect x="307.20000000000005" y="409.6" width="51.2" height="51.2" fill="#e77d84" />
<rect x="307.20000000000005" y="460.8" width="51.2" height="51.2" fill="#fa7a84" />
<rect x="358.40000000000003" y="0.0" width="51.2" height="51.2" fill="#379d8d" />
<rect x="358.40000000000003" y="51.2" width="51.2" height="51.2" fill="#4d968e" />
<rect x="358.40000000000003" y="102.4" width="51.2" height="51.2" fill="#629090" />
<rect x="358.40000000000003" y="153.60000000000002" width="51.2" height="51.2" fill="#788a91" />
<rect x="358.40000000000003" y="204.8" width="51.2" height="51.2" fill="#8e8493" />
<rect x="358.40000000000003" y="256.0" width="51.2" height="51.2" fill="#a47e94" />
<rect x="358.40000000000003" y="307.20000000000005" width="51.2" height="51.2" fill="#ba7796" />
<rect x="358.40000000000003" y="358.40000000000003" width="51.2" height="51.2" fill="#d07197" />
<rect x="358.40000000000003" y="409.6" width="51.2" height="51.2" fill="#e56b99" />
<rect x="358.40000000000003" y="460.8" width="51.2" height="51.2" fill="#fb659a" />
<rect x="409.6" y="0.0" width="51.2" height="51.2" fill="#1ea29b" />
<rect x="409.6" y="51.2" width="51.2" height="51.2" fill="#36999d" />
<rect x="409.6" y="102.4" width="51.2" height="51.2" fill="#4f90a0" />
<rect x="409.6" y="153.60000000000002" width="51.2" height="51.2" fill="#6887a2" />
<rect x="409.6" y="204.8" width="51.2" height="51.2" fill="#817ea5" />
<rect x="409.6" y="256.0" width="51.2" height="51.2" fill="#9a75a7" />
<rect x="409.6" y="307.20000000000005" width="51.2" height="51.2" fill="#b26ba9" />
<rect x="409.6" y="358.40000000000003" width="51.2" height="51.2" fill="#cb62ac" />
<rect x="409.6" y="409.6" width="51.2" height="51.2" fill="#e459ae" />
<rect x="409.6" y="460.8" width="51.2" height="51.2" fill="#fd50b0" />
<rect x="460.8" y="0.0" width="51.2" height="51.2" fill="#05a8aa" />
<rect x="460.8" y="51.2" width="51.2" height="51.2" fill="#209bad" />
<rect x="460.8" y="102.4" width="51.2" height="51.2" fill="#3c90b0" />
<rect x="460.8" y="153.60000000000002" width="51.2" height="51.2" fill="#5884b3" />
<rect x="460.8" y="204.8" width="51.2" height="51.2" fill="#7478b6" />
<rect x="460.8" y="256.0" width="51.2" height="51.2" fill="#8f6cba" />
<rect x="460.8" y="307.20000000000005" width="51.2" height="51.2" fill="#ab60bd" />
<rect x="460.8" y="358.40000000000003" width="51.2" height="51.2" fill="#c754c0" />
<rect x="460.8" y="409.6" width="51.2" height="51.2" fill="#e348c3" />
<rect x="460.8" y="460.8" width="51.2" height="51.2" fill="#ff3cc7" />
</svg>

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,3 +1,73 @@
import EditorDB from "@/types/EditorDB";
export const db = new EditorDB();
export const db = new EditorDB();
// StorageManager code from https://dexie.org/docs/StorageManager
/** Check if storage is persisted already.
@returns {Promise<boolean>} Promise resolved with true if current origin is
using persistent storage, false if not, and undefined if the API is not
present.
*/
export async function isStoragePersisted() : Promise<boolean | undefined> {
return await navigator.storage && navigator.storage.persisted ?
navigator.storage.persisted() :
undefined;
}
/** Tries to convert to persisted storage.
@returns {Promise<boolean>} Promise resolved with true if successfully
persisted the storage, false if not, and undefined if the API is not present.
*/
export async function persist(): Promise<boolean | undefined> {
return await navigator.storage && navigator.storage.persist ?
navigator.storage.persist() :
undefined;
}
/** Queries available disk quota.
@see https://developer.mozilla.org/en-US/docs/Web/API/StorageEstimate
@returns {Promise<{quota: number, usage: number}>} Promise resolved with
{quota: number, usage: number} or undefined.
*/
export async function showEstimatedQuota(): Promise<StorageEstimate | undefined> {
return await navigator.storage && navigator.storage.estimate ?
navigator.storage.estimate() :
undefined;
}
/** Tries to persist storage without ever prompting user.
@returns {Promise<string>}
"never" In case persisting is not ever possible. Caller don't bother
asking user for permission.
"prompt" In case persisting would be possible if prompting user first.
"persisted" In case this call successfully silently persisted the storage,
or if it was already persisted.
*/
export async function tryPersistWithoutPromtingUser() {
if (!navigator.storage || !navigator.storage.persisted) {
return "never";
}
let persisted = await navigator.storage.persisted();
if (persisted) {
return "persisted";
}
if (!navigator.permissions || !navigator.permissions.query) {
return "prompt"; // It MAY be successful to prompt. Don't know.
}
const permission = await navigator.permissions.query({
name: "persistent-storage"
});
if (permission.state === "granted") {
persisted = await navigator.storage.persist();
if (persisted) {
return "persisted";
} else {
throw new Error("Failed to persist");
}
}
if (permission.state === "prompt") {
return "prompt";
}
return "never";
}

7
src/lib/uuid.ts Normal file
View File

@ -0,0 +1,7 @@
export const generateUUID = (): string => (
"randomUUID" in crypto
? crypto.randomUUID()
: "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
)
);

View File

@ -1,11 +1,11 @@
import { Entity } from "dexie";
import type EditorDB from "./EditorDB";
class Project extends Entity<EditorDB> {
export default class Project extends Entity<EditorDB> {
uuid!: string;
name!: string;
origin!: string; // If the project was duplicated, origin will be equal to the UUID of original project
title!: string;
description!: string;
creationDate!: number;
editDate!: number;
}
export default Project;
}