Poubelle par utilisateur (interface) : icône pleine/vide, restauration, vidage, réglage de la purge dans les paramètres
This commit is contained in:
1 parent
8a1417055b
commit
d052ac383b
7 files changed
+277
-42
No files matched your search
@@ -104,7 +104,7 @@ export default function App() {
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [drawerWidth, setDrawerWidth] = useState(initialDrawerWidth);
|
||||
const [resizing, setResizing] = useState(false);
|
||||
const [settings, setSettings] = useState<Settings>({ title: "", logo: "", channel: "latest" });
|
||||
const [settings, setSettings] = useState<Settings>({ title: "", logo: "", channel: "latest", trashDays: 0 });
|
||||
const updateStatus = useUpdateStatus();
|
||||
const pending = pendingVersion(updateStatus, settings.channel);
|
||||
const [customizing, setCustomizing] = useState<false | "all" | "excalidraw">(false);
|
||||
|
||||
+170
-36
@@ -8,8 +8,10 @@ import {
|
||||
displayPath,
|
||||
EXT,
|
||||
inFavoris,
|
||||
inPoubelle,
|
||||
join,
|
||||
label,
|
||||
POUBELLE,
|
||||
stripExt,
|
||||
withExt,
|
||||
type Entry,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
DotsIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
FileIcon,
|
||||
FilePlusIcon,
|
||||
FolderIcon,
|
||||
FolderPlusIcon,
|
||||
@@ -32,12 +35,14 @@ import {
|
||||
ListIcon,
|
||||
MoveIcon,
|
||||
RefreshIcon,
|
||||
RestoreIcon,
|
||||
SearchIcon,
|
||||
SelectAllIcon,
|
||||
ShareIcon,
|
||||
StarFilledIcon,
|
||||
StarIcon,
|
||||
TagIcon,
|
||||
TrashFullIcon,
|
||||
TrashIcon,
|
||||
UpIcon,
|
||||
UploadIcon,
|
||||
@@ -121,6 +126,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
const [tagFilter, setTagFilter] = useState<string | null>(null);
|
||||
const [tagging, setTagging] = useState<Entry[] | null>(null); // éléments du dialogue « Tags »
|
||||
const [iconing, setIconing] = useState<Entry[] | null>(null); // dossiers du dialogue « Icône »
|
||||
const [trashDays, setTrashDays] = useState<number | null>(null); // purge automatique (0 : jamais)
|
||||
const fileInput = useRef<HTMLInputElement>(null);
|
||||
const dirInput = useRef<HTMLInputElement>(null);
|
||||
const stay = useRef<string | null>(null); // schéma ouvert depuis Favoris : on reste dans Favoris
|
||||
@@ -130,6 +136,9 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
const filtering = query.trim() !== "" || tagFilter !== null;
|
||||
// Dossier Favoris : des liens vers les originaux, ni création ni déplacement.
|
||||
const favView = !results && inFavoris(dir);
|
||||
// Poubelle : éléments à restaurer ou à supprimer définitivement, rien d'autre.
|
||||
const trashView = !results && inPoubelle(dir);
|
||||
const locked = favView || trashView; // ni création, ni import, ni déplacement
|
||||
|
||||
const closeMenu = useCallback(() => setMenu(null), []);
|
||||
// La liste change de forme : le bouton « … » d'origine disparaît.
|
||||
@@ -196,6 +205,15 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
if (filtering) setResults(await api.search(query, filter));
|
||||
};
|
||||
|
||||
// Délai de purge, affiché dans la poubelle (réglé dans les paramètres).
|
||||
useEffect(() => {
|
||||
if (!trashView) return;
|
||||
api
|
||||
.settings()
|
||||
.then((s) => setTrashDays(s.trashDays))
|
||||
.catch(() => setTrashDays(null));
|
||||
}, [trashView, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem("explorerView", view);
|
||||
@@ -235,6 +253,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
const onItemClick = (ev: MouseEvent, e: Entry) => {
|
||||
if (!e.special) {
|
||||
if (ev.shiftKey) return selectRange(e.path);
|
||||
if (trashView) return toggle(e.path); // un élément de la poubelle ne s'ouvre pas
|
||||
// Ctrl/Cmd+clic, ou clic quand une sélection est en cours (mode sélection tactile).
|
||||
if (ev.ctrlKey || ev.metaKey || selected.size > 0) return toggle(e.path);
|
||||
}
|
||||
@@ -362,8 +381,14 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
? `${plural(paths.length, "élément")} (dossiers compris avec leur contenu)`
|
||||
: one?.isDir
|
||||
? `le dossier « ${one.name} » et tout son contenu`
|
||||
: `« ${basename(paths[0])} »`;
|
||||
if (!confirm(`Mettre ${what} à la corbeille ?`)) return;
|
||||
: `« ${stripExt(one?.name ?? basename(paths[0]))} »`;
|
||||
if (trashView) {
|
||||
if (!confirm(`Supprimer définitivement ${what} ? Cette action est irréversible.`)) return;
|
||||
for (const p of paths) await api.remove(p, true);
|
||||
clearSelection();
|
||||
return reload();
|
||||
}
|
||||
if (!confirm(`Mettre ${what} à la poubelle ?`)) return;
|
||||
for (const p of paths) {
|
||||
await api.remove(p, true);
|
||||
onDeleted(p);
|
||||
@@ -372,6 +397,36 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
await reload();
|
||||
});
|
||||
|
||||
/** Remet des éléments de la poubelle à leur emplacement d'origine. */
|
||||
const restoreMany = (paths: string[]) =>
|
||||
guard(async () => {
|
||||
const res = await api.restoreTrash(paths);
|
||||
const failed = res.filter((r) => r.error);
|
||||
const done = res.filter((r) => r.to);
|
||||
clearSelection();
|
||||
await reload();
|
||||
if (done.length) {
|
||||
setNotice({
|
||||
text: `${plural(done.length, "élément")} restauré${done.length > 1 ? "s" : ""}`,
|
||||
detail: done.map((r) => displayPath(r.to!)).join("\n"),
|
||||
});
|
||||
}
|
||||
if (failed.length) {
|
||||
const name = (p: string) => stripExt(list.find((e) => e.path === p)?.name ?? basename(p));
|
||||
setError(
|
||||
`${plural(failed.length, "élément")} non restauré${failed.length > 1 ? "s" : ""} — ${failed.map((r) => `${name(r.from)} : ${r.error}`).join(" ; ")}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const emptyTrash = () =>
|
||||
guard(async () => {
|
||||
if (!confirm(`Vider la poubelle (${plural(list.length, "élément")}) ? Cette action est irréversible.`)) return;
|
||||
await api.remove(POUBELLE, true);
|
||||
clearSelection();
|
||||
await reload();
|
||||
});
|
||||
|
||||
/**
|
||||
* Importe fichiers et arborescences dans targetDir : les dossiers sont recréés,
|
||||
* seuls les .excalidraw sont importés, les schémas déjà présents sont conservés.
|
||||
@@ -442,7 +497,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
onDragOver: (ev: DragEvent) => {
|
||||
const external = hasFiles(ev.dataTransfer);
|
||||
if (!external && !ev.dataTransfer.types.includes(DND_TYPE)) return;
|
||||
if (external && inFavoris(targetDir)) return; // pas d'import dans Favoris
|
||||
if (external && (inFavoris(targetDir) || inPoubelle(targetDir))) return; // pas d'import dans Favoris ni la poubelle
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = external ? "copy" : inFavoris(targetDir) ? "link" : "move";
|
||||
setDropTarget(targetDir);
|
||||
@@ -461,6 +516,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
return;
|
||||
}
|
||||
if (inFavoris(targetDir)) addFavorites(paths);
|
||||
else if (inPoubelle(targetDir)) removeMany(paths);
|
||||
else moveMany(paths, targetDir);
|
||||
},
|
||||
});
|
||||
@@ -468,13 +524,15 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
// --- Rendu ---------------------------------------------------------------
|
||||
|
||||
const crumbs = dir ? dir.split("/") : [];
|
||||
const importDir = results || favView ? "" : dir; // pendant une recherche, le dossier courant n'est pas affiché
|
||||
const importDir = results || locked ? "" : dir; // pendant une recherche, le dossier courant n'est pas affiché
|
||||
const where = (p: string) => displayPath(dirname(p)) || "Racine";
|
||||
const deletedInfo = (e: Entry) =>
|
||||
`Supprimé le ${fmtDate(e.modTime)}${e.by ? ` par ${e.by}` : ""}\nEmplacement d'origine : ${e.origin ? where(e.origin) : "inconnu"}`;
|
||||
|
||||
// Dépôt n'importe où dans l'explorateur : import dans le dossier courant.
|
||||
const zoneProps = {
|
||||
onDragOver: (ev: DragEvent) => {
|
||||
if (favView || !hasFiles(ev.dataTransfer)) return;
|
||||
if (locked || !hasFiles(ev.dataTransfer)) return;
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = "copy";
|
||||
setFileDrag(true);
|
||||
@@ -483,7 +541,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
if (!ev.currentTarget.contains(ev.relatedTarget as Node | null)) setFileDrag(false);
|
||||
},
|
||||
onDrop: (ev: DragEvent) => {
|
||||
if (favView || !hasFiles(ev.dataTransfer)) return;
|
||||
if (locked || !hasFiles(ev.dataTransfer)) return;
|
||||
ev.preventDefault();
|
||||
setFileDrag(false);
|
||||
importTree(collectDrop(ev.dataTransfer), importDir);
|
||||
@@ -493,8 +551,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
const big = view === "grid" ? 40 : 20;
|
||||
const icon = (e: Entry) =>
|
||||
e.special ? (
|
||||
<span className={`icon special ${e.special}`} aria-hidden>
|
||||
{e.special === "favoris" ? <StarFilledIcon size={big} /> : <UserIcon size={big} />}
|
||||
<span className={`icon special ${e.special} ${e.count ? "full" : ""}`} aria-hidden>
|
||||
{e.special === "favoris" ? (
|
||||
<StarFilledIcon size={big} />
|
||||
) : e.special === "poubelle" ? (
|
||||
e.count ? (
|
||||
<TrashFullIcon size={big} />
|
||||
) : (
|
||||
<TrashIcon size={big} />
|
||||
)
|
||||
) : (
|
||||
<UserIcon size={big} />
|
||||
)}
|
||||
</span>
|
||||
) : trashView ? (
|
||||
<span className="icon" aria-hidden>
|
||||
{e.isDir ? <FolderIcon size={big} /> : <FileIcon size={big} />}
|
||||
</span>
|
||||
) : e.broken ? (
|
||||
<span className="icon folder" aria-hidden>
|
||||
@@ -543,6 +615,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
</>
|
||||
);
|
||||
|
||||
// Dans la poubelle : restauration ou suppression définitive.
|
||||
const trashActions = (e: Entry) => (
|
||||
<>
|
||||
<button role="menuitem" onClick={() => restoreMany([e.path])}>
|
||||
<RestoreIcon />
|
||||
Restaurer
|
||||
</button>
|
||||
<hr />
|
||||
<button role="menuitem" className="danger" onClick={() => removeMany([e.path])}>
|
||||
<TrashIcon />
|
||||
Supprimer définitivement
|
||||
</button>
|
||||
<span className="muted size deleted">{deletedInfo(e)}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const actions = (e: Entry) => (
|
||||
<>
|
||||
<button role="menuitem" onClick={() => rename(e)}>
|
||||
@@ -617,7 +705,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
<li key={e.path} className={cls}>
|
||||
<div
|
||||
className="row"
|
||||
draggable={!e.special && !favView}
|
||||
draggable={!e.special && !locked}
|
||||
onDragStart={(ev) => onDragStart(ev, e)}
|
||||
{...(e.isDir ? dropProps(e.path) : {})}
|
||||
>
|
||||
@@ -640,7 +728,17 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
<button
|
||||
className="name"
|
||||
onClick={(ev) => onItemClick(ev, e)}
|
||||
title={e.broken ? "Original introuvable : retirer ce favori" : displayPath(e.path)}
|
||||
title={
|
||||
e.broken
|
||||
? "Original introuvable : retirer ce favori"
|
||||
: trashView
|
||||
? deletedInfo(e)
|
||||
: e.special === "poubelle"
|
||||
? e.count
|
||||
? `Poubelle : ${plural(e.count, "élément")}`
|
||||
: "Poubelle vide"
|
||||
: displayPath(e.path)
|
||||
}
|
||||
>
|
||||
{icon(e)}
|
||||
<span className="label">
|
||||
@@ -650,7 +748,9 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
</span>
|
||||
)}
|
||||
{e.isDir ? e.name : stripExt(e.name)}
|
||||
{e.special === "poubelle" && !!e.count && <small className="badge">{e.count}</small>}
|
||||
{(results || (favView && !e.broken)) && <small className="muted where"> {where(e.path)}</small>}
|
||||
{trashView && <small className="muted where"> {e.origin ? where(e.origin) : ""}</small>}
|
||||
{e.snippet && <small className="snippet">{e.snippet}</small>}
|
||||
</span>
|
||||
</button>
|
||||
@@ -668,7 +768,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{!e.isDir && view === "list" && <span className="meta muted">{fmtDate(e.modTime)}</span>}
|
||||
{(!e.isDir || trashView) && view === "list" && <span className="meta muted">{fmtDate(e.modTime)}</span>}
|
||||
{!e.special && (
|
||||
<button
|
||||
className={`icon-btn more ${menu?.entry.path === e.path ? "open" : ""}`}
|
||||
@@ -692,18 +792,18 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
return (
|
||||
<div className={`explorer ${fileDrag ? "file-drag" : ""}`} onKeyDown={onKeyDown} {...zoneProps}>
|
||||
<div className="toolbar">
|
||||
<button className="ex-btn" onClick={newDrawing} title="Nouveau schéma" disabled={favView}>
|
||||
<button className="ex-btn" onClick={newDrawing} title="Nouveau schéma" disabled={locked}>
|
||||
<FilePlusIcon />
|
||||
<span>Schéma</span>
|
||||
</button>
|
||||
<button className="ex-btn" onClick={newFolder} title="Nouveau dossier" disabled={favView}>
|
||||
<button className="ex-btn" onClick={newFolder} title="Nouveau dossier" disabled={locked}>
|
||||
<FolderPlusIcon />
|
||||
<span>Dossier</span>
|
||||
</button>
|
||||
<button
|
||||
className="ex-btn"
|
||||
onClick={() => fileInput.current?.click()}
|
||||
disabled={favView}
|
||||
disabled={locked}
|
||||
title="Importer des fichiers .excalidraw (ou glisser-déposer fichiers et dossiers ici)"
|
||||
>
|
||||
<UploadIcon />
|
||||
@@ -712,7 +812,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
<button
|
||||
className="ex-btn square"
|
||||
onClick={() => dirInput.current?.click()}
|
||||
disabled={favView}
|
||||
disabled={locked}
|
||||
title="Importer un dossier complet (arborescence recréée, seuls les .excalidraw sont importés)"
|
||||
aria-label="Importer un dossier"
|
||||
>
|
||||
@@ -812,6 +912,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{trashView && (
|
||||
<div className="trashbar">
|
||||
<small className="muted">
|
||||
{trashDays === null
|
||||
? ""
|
||||
: trashDays === 0
|
||||
? "Conservés jusqu'à suppression définitive"
|
||||
: `Supprimés définitivement après ${plural(trashDays, "jour")}`}
|
||||
</small>
|
||||
<button className="ex-btn danger" onClick={emptyTrash} disabled={list.length === 0}>
|
||||
<TrashIcon />
|
||||
<span>Vider la poubelle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected.size > 0 && (
|
||||
<div className="selbar">
|
||||
<button className="icon-btn" onClick={clearSelection} aria-label="Annuler la sélection" title="Annuler la sélection (Échap)">
|
||||
@@ -826,23 +942,33 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
>
|
||||
{selected.size === selectable.length ? <DeselectIcon /> : <SelectAllIcon />}
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={() => toggleFavorite(list.filter((e) => selected.has(e.path)))}
|
||||
title={favView ? "Retirer des favoris" : "Favoris (ajouter ou retirer)"}
|
||||
aria-label="Favoris"
|
||||
>
|
||||
<StarIcon />
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={() => setTagging(list.filter((e) => selected.has(e.path)))}
|
||||
title="Tags…"
|
||||
aria-label="Tags"
|
||||
>
|
||||
<TagIcon />
|
||||
</button>
|
||||
{!favView && (
|
||||
{trashView && (
|
||||
<button className="ex-btn" onClick={() => restoreMany([...selected])}>
|
||||
<RestoreIcon />
|
||||
<span>Restaurer</span>
|
||||
</button>
|
||||
)}
|
||||
{!trashView && (
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={() => toggleFavorite(list.filter((e) => selected.has(e.path)))}
|
||||
title={favView ? "Retirer des favoris" : "Favoris (ajouter ou retirer)"}
|
||||
aria-label="Favoris"
|
||||
>
|
||||
<StarIcon />
|
||||
</button>
|
||||
)}
|
||||
{!trashView && (
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={() => setTagging(list.filter((e) => selected.has(e.path)))}
|
||||
title="Tags…"
|
||||
aria-label="Tags"
|
||||
>
|
||||
<TagIcon />
|
||||
</button>
|
||||
)}
|
||||
{!locked && (
|
||||
<button className="ex-btn" onClick={() => setMoving([...selected])}>
|
||||
<MoveIcon />
|
||||
<span>Déplacer…</span>
|
||||
@@ -851,8 +977,10 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
<button
|
||||
className="icon-btn danger"
|
||||
onClick={() => removeMany([...selected])}
|
||||
title={favView ? "Retirer des favoris (Suppr)" : "Mettre à la corbeille (Suppr)"}
|
||||
aria-label={favView ? "Retirer des favoris" : "Supprimer"}
|
||||
title={
|
||||
favView ? "Retirer des favoris (Suppr)" : trashView ? "Supprimer définitivement (Suppr)" : "Mettre à la poubelle (Suppr)"
|
||||
}
|
||||
aria-label={favView ? "Retirer des favoris" : trashView ? "Supprimer définitivement" : "Supprimer"}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
@@ -887,7 +1015,13 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
{list.map(item)}
|
||||
{list.length === 0 && (
|
||||
<li className="muted empty">
|
||||
{results ? "Aucun résultat" : favView ? "Aucun favori : étoile ou glisser-déposer un schéma ici" : "Dossier vide"}
|
||||
{results
|
||||
? "Aucun résultat"
|
||||
: favView
|
||||
? "Aucun favori : étoile ou glisser-déposer un schéma ici"
|
||||
: trashView
|
||||
? "La poubelle est vide"
|
||||
: "Dossier vide"}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
@@ -913,7 +1047,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
|
||||
|
||||
{menu && (
|
||||
<ActionMenu anchor={menu.anchor} onClose={closeMenu}>
|
||||
{favView ? favActions(menu.entry) : actions(menu.entry)}
|
||||
{favView ? favActions(menu.entry) : trashView ? trashActions(menu.entry) : actions(menu.entry)}
|
||||
</ActionMenu>
|
||||
)}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function MoveDialog({ paths, startDir, onCancel, onConfirm }: Props) {
|
||||
setError(null);
|
||||
api
|
||||
.list(dir)
|
||||
.then((es) => live && setFolders(es.filter((e) => e.isDir && e.special !== "favoris")))
|
||||
.then((es) => live && setFolders(es.filter((e) => e.isDir && e.special !== "favoris" && e.special !== "poubelle")))
|
||||
.catch((e) => live && setError(e.message));
|
||||
return () => {
|
||||
live = false;
|
||||
|
||||
@@ -26,6 +26,14 @@ const CHANNELS: { id: Channel; label: string; hint: string }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Délais de purge de la poubelle (0 : jamais). */
|
||||
const TRASH_DAYS = [
|
||||
{ days: 0, label: "Jamais" },
|
||||
{ days: 1, label: "1 jour" },
|
||||
{ days: 7, label: "7 jours" },
|
||||
{ days: 30, label: "30 jours" },
|
||||
];
|
||||
|
||||
/** Paramètres partagés par tous les utilisateurs : titre, logo, version d'Excalidraw. */
|
||||
export function SettingsDialog({ settings, update, focusExcalidraw, onChange, onClose }: Props) {
|
||||
const [title, setTitle] = useState(settings.title);
|
||||
@@ -214,6 +222,31 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="field">
|
||||
<h4>Poubelle</h4>
|
||||
<span>Suppression définitive automatique</span>
|
||||
<div className="radio-group" role="radiogroup" aria-label="Purge de la poubelle">
|
||||
{TRASH_DAYS.map((d) => (
|
||||
<button
|
||||
key={d.days}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={settings.trashDays === d.days}
|
||||
className={settings.trashDays === d.days ? "on" : ""}
|
||||
disabled={busy}
|
||||
onClick={() => settings.trashDays !== d.days && run(() => api.setTrashDays(d.days))}
|
||||
>
|
||||
{d.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<small className="muted">
|
||||
{settings.trashDays
|
||||
? `Les éléments mis à la poubelle sont supprimés définitivement ${settings.trashDays} jour${settings.trashDays > 1 ? "s" : ""} après leur suppression.`
|
||||
: "La poubelle garde tout jusqu'à ce qu'on la vide ; son icône indique si elle contient des éléments."}
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section className="field">
|
||||
<h4>Liens de partage</h4>
|
||||
{shares?.length ? (
|
||||
|
||||
+22
-4
@@ -10,8 +10,14 @@ export interface Entry {
|
||||
favorite?: boolean;
|
||||
/** Dossier : icône choisie (voir folderIcons.tsx). */
|
||||
icon?: string;
|
||||
/** Entrée virtuelle de la racine : "favoris" ou "perso" (espace personnel). */
|
||||
special?: "favoris" | "perso";
|
||||
/** Entrée virtuelle de la racine : "favoris", "perso" (espace personnel) ou "poubelle". */
|
||||
special?: "favoris" | "perso" | "poubelle";
|
||||
/** Entrée « poubelle » de la racine : nombre d'éléments qu'elle contient. */
|
||||
count?: number;
|
||||
/** Élément de la poubelle : chemin d'origine (modTime = date de suppression). */
|
||||
origin?: string;
|
||||
/** Élément de la poubelle : supprimé par (vide si inconnu). */
|
||||
by?: string;
|
||||
/** Dans le dossier Favoris : nom du lien (le chemin est celui de l'original). */
|
||||
link?: string;
|
||||
/** Favori dont l'original a disparu (chemin .favoris/<lien>, à retirer). */
|
||||
@@ -43,11 +49,12 @@ export interface MoveResult {
|
||||
/** Canal npm d'Excalidraw : latest (stable) ou next (préversion). */
|
||||
export type Channel = "latest" | "next";
|
||||
|
||||
/** Paramètres partagés ; logo = "" sans logo, sinon sa version. */
|
||||
/** Paramètres partagés ; logo = "" sans logo, sinon sa version ; trashDays = 0 : poubelle jamais purgée. */
|
||||
export interface Settings {
|
||||
title: string;
|
||||
logo: string;
|
||||
channel: Channel;
|
||||
trashDays: number;
|
||||
}
|
||||
|
||||
/** Versions d'Excalidraw publiées sur npm par canal (vide si inconnu ou désactivé). */
|
||||
@@ -184,10 +191,15 @@ export const api = {
|
||||
copy: async (from: string, to: string): Promise<Entry> =>
|
||||
(await request("/api/copy", json({ from, to }))).json(),
|
||||
|
||||
/** Met à la poubelle ; dans .poubelle, supprime définitivement (path = .poubelle : la vide). */
|
||||
remove: async (path: string, recursive: boolean): Promise<void> => {
|
||||
await request(`/api/entry?path=${q(path)}&recursive=${recursive}`, { method: "DELETE" });
|
||||
},
|
||||
|
||||
/** Remet des éléments de la poubelle à leur emplacement d'origine (to : chemin restauré). */
|
||||
restoreTrash: async (paths: string[]): Promise<MoveResult[]> =>
|
||||
(await request("/api/trash/restore", json({ paths }))).json(),
|
||||
|
||||
history: async (path: string): Promise<Version[]> =>
|
||||
(await request(`/api/history?path=${q(path)}`)).json(),
|
||||
|
||||
@@ -253,6 +265,10 @@ export const api = {
|
||||
setChannel: async (channel: Channel): Promise<Settings> =>
|
||||
(await request("/api/settings", { ...json({ channel }), method: "PUT" })).json(),
|
||||
|
||||
/** days = 0 : la poubelle n'est jamais purgée. */
|
||||
setTrashDays: async (trashDays: number): Promise<Settings> =>
|
||||
(await request("/api/settings", { ...json({ trashDays }), method: "PUT" })).json(),
|
||||
|
||||
/** Le serveur recadre au centre et ramène le PNG à 64x64. */
|
||||
uploadLogo: async (png: Blob): Promise<Settings> =>
|
||||
(
|
||||
@@ -285,12 +301,14 @@ export const join = (dir: string, name: string) => (dir ? `${dir}/${name}` : nam
|
||||
// Dossiers propres à l'utilisateur connecté (voir internal/storage/spaces.go).
|
||||
export const PERSO = ".perso";
|
||||
export const FAVORIS = ".favoris";
|
||||
const SPECIAL_LABELS: Record<string, string> = { [PERSO]: "Personnel", [FAVORIS]: "Favoris" };
|
||||
export const POUBELLE = ".poubelle";
|
||||
const SPECIAL_LABELS: Record<string, string> = { [PERSO]: "Personnel", [FAVORIS]: "Favoris", [POUBELLE]: "Poubelle" };
|
||||
/** Libellé d'un composant de chemin (".perso" → "Personnel"). */
|
||||
export const label = (part: string) => SPECIAL_LABELS[part] ?? part;
|
||||
/** Chemin affiché : "Personnel/projets" pour ".perso/projets". */
|
||||
export const displayPath = (p: string) => p.split("/").map(label).join("/");
|
||||
export const inFavoris = (p: string) => p === FAVORIS || p.startsWith(FAVORIS + "/");
|
||||
export const inPoubelle = (p: string) => p === POUBELLE || p.startsWith(POUBELLE + "/");
|
||||
export const EXT = ".excalidraw";
|
||||
export const withExt = (name: string) => (name.toLowerCase().endsWith(EXT) ? name : name + EXT);
|
||||
export const stripExt = (name: string) =>
|
||||
|
||||
@@ -112,6 +112,24 @@ export const TrashIcon = icon(
|
||||
<path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />,
|
||||
);
|
||||
|
||||
/** Poubelle qui contient des éléments : une feuille et une boule de papier dépassent du bord. */
|
||||
export const TrashFullIcon = icon(
|
||||
<>
|
||||
<path d="M4 10h16M5 10l1 9a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-9M10 14v3M14 14v3" />
|
||||
<path d="M7.5 10 9 3.5l4.5 1-1.2 5.5M10.2 6.5l2 .5" />
|
||||
<circle cx="16" cy="7.8" r="2.2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
export const RestoreIcon = icon(<path d="M9 14l-4-4 4-4M5 10h11a4 4 0 1 1 0 8h-1" />);
|
||||
|
||||
export const FileIcon = icon(
|
||||
<>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z" />
|
||||
</>,
|
||||
);
|
||||
|
||||
export const SelectAllIcon = icon(
|
||||
<>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
|
||||
@@ -1526,3 +1526,35 @@ button.chip.on,
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Poubelle : icône pleine ou vide, nombre d'éléments, barre « Vider ». */
|
||||
.icon.special.poubelle {
|
||||
color: var(--muted);
|
||||
}
|
||||
.icon.special.poubelle.full {
|
||||
color: var(--accent);
|
||||
}
|
||||
.row .label .badge {
|
||||
display: inline-block;
|
||||
min-width: 18px;
|
||||
margin-left: 6px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
.trashbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.menu-popup .size.deleted {
|
||||
max-width: 260px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
Reference in new issue
Block a user