Add Web Push, guided start screen, SVG icons, chunked uploads, rename
- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished notifications while no app window is visible; settings with test button - Guided new-session flow: task, craft target (website/app/game/old software/fresh), clean room or deep, description, model, folder (new, existing or cloned git repository); originals upload to original/ - Craft phase bar (craft_phase tool) for study/specify/build/check/ready - Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB) - All UI glyphs and emojis replaced by themeable SVG icons - Rename dialog with manual name or model-generated suggestion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
013695722a
commit
a0d0e707f2
48 files changed
+2724
-228
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { Icon } from "./Icon";
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { del, get, post, put } from "../api";
|
||||
import { useFetch } from "../hooks";
|
||||
@@ -45,13 +46,13 @@ const baseName = (p: string) => p.split("/").filter(Boolean).pop() ?? p;
|
||||
const dirName = (p: string) => p.replace(/\/[^/]+\/?$/, "") || "/";
|
||||
|
||||
function icon(e: Entry) {
|
||||
if (e.is_dir) return "📁";
|
||||
if (IMAGE.test(e.name)) return "🖼";
|
||||
if (/\.(wav|mp3|ogg|flac|m4a|xm|mod|s3m|it)$/i.test(e.name)) return "🎵";
|
||||
if (/\.(glb|gltf|stl|obj|step|stp|igs|iges)$/i.test(e.name)) return "🧊";
|
||||
if (/\.(pdf|docx|pptx|xlsx|xls|ods)$/i.test(e.name)) return "📑";
|
||||
if (/\.(md|txt)$/i.test(e.name)) return "📝";
|
||||
return "📄";
|
||||
if (e.is_dir) return <Icon name="folder" className="ficon dir" />;
|
||||
if (IMAGE.test(e.name)) return <Icon name="image" className="ficon" />;
|
||||
if (/\.(wav|mp3|ogg|flac|m4a|xm|mod|s3m|it)$/i.test(e.name)) return <Icon name="music" className="ficon" />;
|
||||
if (/\.(glb|gltf|stl|obj|step|stp|igs|iges)$/i.test(e.name)) return <Icon name="cube" className="ficon" />;
|
||||
if (/\.(pdf|docx|pptx|xlsx|xls|ods)$/i.test(e.name)) return <Icon name="file-doc" className="ficon" />;
|
||||
if (/\.(md|txt)$/i.test(e.name)) return <Icon name="file-text" className="ficon" />;
|
||||
return <Icon name="file" className="ficon" />;
|
||||
}
|
||||
|
||||
export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
@@ -243,13 +244,13 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
)}
|
||||
<input className="files-search" placeholder={t("files.search")} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<button className="small" title={t("files.newFile")} onClick={() => newEntry(false)}>
|
||||
+ 📄
|
||||
<Icon name="plus" className="tiny" /> <Icon name="file" />
|
||||
</button>
|
||||
<button className="small" title={t("files.newFolder")} onClick={() => newEntry(true)}>
|
||||
+ 📁
|
||||
<Icon name="plus" className="tiny" /> <Icon name="folder" />
|
||||
</button>
|
||||
<button className="small" title={t("files.upload")} onClick={() => uploadRef.current?.click()}>
|
||||
⤒
|
||||
<Icon name="upload" />
|
||||
</button>
|
||||
<input ref={uploadRef} type="file" multiple hidden onChange={(e) => e.target.files && upload(e.target.files)} />
|
||||
<button className={`small ${hidden ? "active" : ""}`} title={t("files.showHidden")} onClick={() => setHidden(!hidden)}>
|
||||
@@ -277,7 +278,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
) : (
|
||||
hits.map((h) => (
|
||||
<button key={h.path} className="entry" onClick={() => (h.is_dir ? load(h.path) : openFile(h.path))}>
|
||||
<span>{h.is_dir ? "📁" : "📄"}</span>
|
||||
<Icon name={h.is_dir ? "folder" : "file"} className="ficon" />
|
||||
<span className="name">{h.path.slice((listing?.path.length ?? 0) + 1)}</span>
|
||||
</button>
|
||||
))
|
||||
@@ -286,7 +287,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
<>
|
||||
{listing?.parent && (
|
||||
<button className="entry" onClick={() => load(listing.parent!)}>
|
||||
<span>↰</span>
|
||||
<Icon name="folder-up" className="ficon" />
|
||||
<span className="name">..</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -298,11 +299,11 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
<button className="entry-main" onClick={() => (e.is_dir ? load(e.path) : openFile(e.path))}>
|
||||
<span>{icon(e)}</span>
|
||||
<span className="name">{e.name}</span>
|
||||
{mark && <span className={`git-mark m-${mark === "•" ? "dot" : mark}`}>{mark}</span>}
|
||||
{mark && <span className={`git-mark m-${mark === "•" ? "dot" : mark}`}>{mark === "•" ? <Icon name="dot" className="tiny" /> : mark}</span>}
|
||||
{!e.is_dir && <span className="size">{fmtCompact(e.size)}B</span>}
|
||||
</button>
|
||||
<details className="entry-menu">
|
||||
<summary aria-label={t("files.actions")}>⋯</summary>
|
||||
<summary aria-label={t("files.actions")}><Icon name="more" /></summary>
|
||||
<div className="menu">
|
||||
<button onClick={() => rename(e)}>{t("files.rename")}</button>
|
||||
<button onClick={() => duplicate(e)}>{t("files.duplicate")}</button>
|
||||
@@ -333,7 +334,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
<div className="editor-pane">
|
||||
<div className="editor-bar">
|
||||
<button className="ghost small mobile-only" onClick={() => setMobileEditor(false)}>
|
||||
←
|
||||
<Icon name="arrow-left" />
|
||||
</button>
|
||||
<b className="grow">{pane.title}</b>
|
||||
</div>
|
||||
@@ -349,11 +350,11 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
<div className="editor-pane">
|
||||
<div className="editor-bar">
|
||||
<button className="ghost small mobile-only" onClick={() => void confirmDiscard().then((ok): unknown => ok && setMobileEditor(false))}>
|
||||
←
|
||||
<Icon name="arrow-left" />
|
||||
</button>
|
||||
<b className="grow" title={f.path}>
|
||||
{baseName(f.path)}
|
||||
{dirty && <span className="dirty"> ●</span>}
|
||||
{dirty && <span className="dirty"> <Icon name="dot" className="tiny" /></span>}
|
||||
</b>
|
||||
{(isMd || isImage) && !f.preview && (
|
||||
<button className="small" onClick={() => setPreview(!preview)}>
|
||||
@@ -361,7 +362,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
</button>
|
||||
)}
|
||||
<a className="button small" href={rawUrl(f.path, true)}>
|
||||
⤓
|
||||
<Icon name="download" />
|
||||
</a>
|
||||
{!f.binary && (
|
||||
<button className="small primary" disabled={!dirty} onClick={() => save()}>
|
||||
@@ -400,7 +401,7 @@ export function Files({ onMenu }: { onMenu: () => void }) {
|
||||
<>
|
||||
<div className="topbar">
|
||||
<button className="ghost menu-btn" onClick={onMenu} aria-label={t("nav.menu")}>
|
||||
☰
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
<div className="grow">
|
||||
<h1>{t("nav.files")}</h1>
|
||||
|
||||
Reference in new issue
Block a user