"use client"; import React from "react"; import { useRouter } from "next/navigation"; import { addLayer, createLayer, createProject, parseProjectFile, setCanvasSize } from "@pien-studio/editor-core"; import type { Project } from "@pien-studio/types"; import { deleteProject, duplicateProject, loadProjects, upsertProject } from "@pien-studio/storage"; import { useEditorStore } from "../store/editor-store"; import { useUiStore } from "../store/ui-store"; import { accentButtonClass, cx, mutedSurfaceClass, subtleButtonClass, surfaceClass } from "../lib/theme"; import { UiPreferences } from "../components/ui-preferences"; import { useAssetCleanupJob } from "../hooks/use-asset-cleanup-job"; import { useTranslations } from "../hooks/use-translations"; export default function HomePage() { useAssetCleanupJob(); const router = useRouter(); const setProject = useEditorStore((s) => s.setProject); const { theme, hydrate } = useUiStore((s) => s); const { t } = useTranslations(); const [projects, setProjects] = React.useState([]); const [showWipModal, setShowWipModal] = React.useState(true); const [projectPendingDelete, setProjectPendingDelete] = React.useState(null); const projectInputRef = React.useRef(null); const imageInputRef = React.useRef(null); const isDark = theme === "dark"; const refreshProjects = React.useCallback(async () => { setProjects(await loadProjects()); }, []); React.useEffect(() => { hydrate(); void refreshProjects(); }, [hydrate, refreshProjects]); function openProject(project: Project) { setProject(project); router.push(`/editor/${project.id}`); } async function confirmDeleteProject() { if (!projectPendingDelete) return; await deleteProject(projectPendingDelete.id); await refreshProjects(); setProjectPendingDelete(null); } async function handleNewProject() { const project = createProject(t("home.untitledProject")); await upsertProject(project); openProject(project); } async function handleImportProjectFile(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; const raw = await file.text(); try { const parsed = parseProjectFile(raw); if (!parsed.ok) return; await upsertProject(parsed.project); await refreshProjects(); openProject(parsed.project); } finally { event.target.value = ""; } } function handleOpenImage(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; const title = file.name.replace(/\.[^/.]+$/, "") || t("home.imageProject"); const reader = new FileReader(); reader.onload = async () => { try { const sourceUri = typeof reader.result === "string" ? reader.result : undefined; if (!sourceUri) return; const imageSize = await new Promise<{ width: number; height: number }>((resolve) => { const image = new Image(); image.onload = () => { resolve({ width: Math.max(1, Math.round(image.naturalWidth)), height: Math.max(1, Math.round(image.naturalHeight)), }); }; image.onerror = () => resolve({ width: 1, height: 1 }); image.src = sourceUri; }); const base = createProject(title, "free"); const projectWithImageCanvas = setCanvasSize(base, imageSize.width, imageSize.height); const project = addLayer(projectWithImageCanvas, createLayer("image", { name: file.name, sourceUri, x: 0, y: 0, width: imageSize.width, height: imageSize.height, })); await upsertProject(project); await refreshProjects(); openProject(project); } finally { event.target.value = ""; } }; reader.readAsDataURL(file); } return ( <> {showWipModal ? (
setShowWipModal(false)}>
event.stopPropagation()} >

{t("home.wipTitle")}

{t("home.wipBody")}

{t("home.wipSupportPrefix")} {" "} github.com/sponsors/YuzuZensai

) : null} {projectPendingDelete ? (
setProjectPendingDelete(null)}>
event.stopPropagation()} >

Delete project?

This will permanently delete {projectPendingDelete.title} from local storage.

) : null}

pien.studio

{t("home.projectHub")}

{t("home.createOpenManage")}

{t("home.myProjects")}

{projects.length === 0 ?

{t("home.noProjectsYet")}

: null}
{projects.map((project) => (

{project.title}

{new Date(project.updatedAt).toLocaleString()}

))}
); }