feat(wallpaper): extend commands with gallery, delete image, and crop
The plugin now exposes gallery retrieval, image deletion and image crop across Android, desktop and mobile. The Rust core and Android bridge are wired to handle these commands, updating state as needed. - Add gallery, delete_image, and set_image_crop commands - across Android, desktop and mobile. - Update permissions, defaults, and schema to enable new commands.
This commit is contained in:
+47
-11
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, ChevronRight, Home, Images, LockKeyhole, Settings, Shuffle, Smartphone, Sparkles } from "lucide-react";
|
||||
import { getState, nextWallpaper, selectImages, setSetting, type WallpaperState } from "./native";
|
||||
import { ArrowLeft, Check, ChevronRight, Home, Images, LockKeyhole, Plus, Settings, Shuffle, Smartphone, Sparkles, Trash2 } from "lucide-react";
|
||||
import { deleteImage, getGallery, getState, nextWallpaper, selectImages, setSetting, type GalleryImage, type WallpaperState } from "./native";
|
||||
|
||||
const initial: WallpaperState = { imageCount: 0, enabled: false, shuffle: true, lockScreenOnly: true, currentIndex: 0, imageUrls: [] };
|
||||
|
||||
@@ -14,13 +14,27 @@ function SettingRow({ icon, label, value, onChange }: { icon: React.ReactNode; l
|
||||
|
||||
export default function App() {
|
||||
const [state, setState] = useState(initial);
|
||||
const [tab, setTab] = useState<"home" | "settings">("home");
|
||||
const [tab, setTab] = useState<"home" | "settings" | "gallery">("home");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [gallery, setGallery] = useState<GalleryImage[]>([]);
|
||||
const [galleryTotal, setGalleryTotal] = useState(0);
|
||||
const [galleryLoading, setGalleryLoading] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState("");
|
||||
|
||||
useEffect(() => { getState().then(setState).catch(() => setState(initial)); }, []);
|
||||
const current = state.imageUrls[state.currentIndex] ?? "/wallpapers/alpine.png";
|
||||
const photos = useMemo(() => state.imageUrls.length ? state.imageUrls : ["/wallpapers/alpine.png", "/wallpapers/waterfall.png", "/wallpapers/coast.png"], [state.imageUrls]);
|
||||
const photos = useMemo(() => state.imageUrls, [state.imageUrls]);
|
||||
|
||||
async function loadGallery(offset = 0, append = false) {
|
||||
setGalleryLoading(true);
|
||||
try {
|
||||
const page = await getGallery(offset, 48);
|
||||
setGallery(previous => append ? [...previous, ...page.items] : page.items);
|
||||
setGalleryTotal(page.total);
|
||||
} catch { setNotice("Galerie konnte nicht geladen werden"); }
|
||||
finally { setGalleryLoading(false); }
|
||||
}
|
||||
|
||||
async function update(name: "enabled" | "shuffle" | "lockScreenOnly", value: boolean) {
|
||||
setState(prev => ({ ...prev, [name]: value }));
|
||||
@@ -29,10 +43,26 @@ export default function App() {
|
||||
|
||||
async function choose() {
|
||||
setBusy(true);
|
||||
try { setState(await selectImages()); setNotice("Bilder wurden zur Sammlung hinzugefügt"); } catch (error) { setNotice(String(error).includes("cancel") ? "Auswahl abgebrochen" : "Bildauswahl ist auf Android verfügbar"); }
|
||||
try {
|
||||
setState(await selectImages());
|
||||
if (tab === "gallery") await loadGallery();
|
||||
setNotice("Bilder wurden zur Sammlung hinzugefügt");
|
||||
} catch (error) { setNotice(String(error).includes("cancel") ? "Auswahl abgebrochen" : "Bildauswahl ist auf Android verfügbar"); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function remove(image: GalleryImage) {
|
||||
if (!window.confirm("Möchtest du dieses Bild wirklich aus deiner Sammlung löschen?")) return;
|
||||
setDeletingId(image.id);
|
||||
try {
|
||||
setState(await deleteImage(image.id));
|
||||
setGallery(previous => previous.filter(item => item.id !== image.id));
|
||||
setGalleryTotal(previous => Math.max(0, previous - 1));
|
||||
setNotice("Bild wurde gelöscht");
|
||||
} catch { setNotice("Bild konnte nicht gelöscht werden"); }
|
||||
finally { setDeletingId(""); }
|
||||
}
|
||||
|
||||
async function next() {
|
||||
setBusy(true);
|
||||
try { setState(await nextWallpaper()); setNotice("Sperrbildschirm wurde aktualisiert"); } catch { setState(prev => ({ ...prev, currentIndex: (prev.currentIndex + 1) % Math.max(1, photos.length) })); }
|
||||
@@ -40,7 +70,8 @@ export default function App() {
|
||||
}
|
||||
|
||||
return <main className="app-shell">
|
||||
<header><div><h1>LockScreenWallpaper</h1><p>{state.enabled ? "Deine Motive wechseln automatisch" : "Automatischer Wechsel ist pausiert"}</p></div><button className="icon-button" aria-label="Einstellungen" onClick={() => setTab("settings")}><Settings /></button></header>
|
||||
{tab === "gallery" ? <header className="gallery-header"><button className="icon-button" aria-label="Zurück" onClick={() => setTab("home")}><ArrowLeft /></button><div><h1>Meine Sammlung</h1><p>{galleryTotal} {galleryTotal === 1 ? "Bild" : "Bilder"}</p></div><button className="icon-button" aria-label="Bilder hinzufügen" onClick={choose} disabled={busy}><Plus /></button></header> :
|
||||
<header><div><h1>LockScreenWallpaper</h1><p>{state.enabled ? "Deine Motive wechseln automatisch" : "Automatischer Wechsel ist pausiert"}</p></div><button className="icon-button" aria-label="Einstellungen" onClick={() => setTab("settings")}><Settings /></button></header>}
|
||||
|
||||
<div className="content">
|
||||
{tab === "home" ? <>
|
||||
@@ -55,16 +86,21 @@ export default function App() {
|
||||
|
||||
<button className="primary" onClick={choose} disabled={busy}><Images />{busy ? "Bitte warten …" : "Bilder auswählen"}</button>
|
||||
|
||||
<section className="collection"><div className="section-heading"><h2>Meine Sammlung</h2><button>{state.imageCount || photos.length} Bilder <ChevronRight /></button></div>
|
||||
<div className="photo-rail">{photos.map((photo, index) => <button key={`${photo}-${index}`} className={index === state.currentIndex ? "selected" : ""} onClick={() => setState(prev => ({ ...prev, currentIndex: index }))}><img src={photo} alt={`Motiv ${index + 1}`} />{index === state.currentIndex && <span><Check /></span>}</button>)}</div>
|
||||
<p className="hint">Tippe auf ein Bild, um es als Vorschau zu sehen.</p>
|
||||
<section className="collection"><div className="section-heading"><h2>Meine Sammlung</h2><button onClick={() => { setTab("gallery"); void loadGallery(); }}>{state.imageCount} {state.imageCount === 1 ? "Bild" : "Bilder"} <ChevronRight /></button></div>
|
||||
{photos.length ? <div className="photo-rail">{photos.map((photo, index) => <button key={`${photo}-${index}`} className={index === state.currentIndex ? "selected" : ""} onClick={() => setState(prev => ({ ...prev, currentIndex: index }))}><img src={photo} alt={`Motiv ${index + 1}`} />{index === state.currentIndex && <span><Check /></span>}</button>)}</div> : <button className="empty-collection" onClick={choose}><Images /><span>Noch keine Bilder ausgewählt</span></button>}
|
||||
<p className="hint">Tippe auf die Bildanzahl, um deine Galerie zu öffnen.</p>
|
||||
</section>
|
||||
|
||||
<section className="settings-list"><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></section>
|
||||
</> : <section className="settings-page"><h2>Einstellungen</h2><p>Lege fest, wie LockScreenWallpaper im Hintergrund arbeitet.</p><div className="settings-list"><SettingRow icon={<Smartphone />} label="Bei jedem Aktivieren wechseln" value={state.enabled} onChange={v => update("enabled", v)} /><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div><div className="info"><h3>Ohne Bilderlimit</h3><p>Deine Auswahl wird privat auf dem Gerät gespeichert. Die einzige Grenze ist der freie Speicherplatz.</p></div></section>}
|
||||
</> : tab === "settings" ? <section className="settings-page"><h2>Einstellungen</h2><p>Lege fest, wie LockScreenWallpaper im Hintergrund arbeitet.</p><div className="settings-list"><SettingRow icon={<Smartphone />} label="Bei jedem Aktivieren wechseln" value={state.enabled} onChange={v => update("enabled", v)} /><SettingRow icon={<Shuffle />} label="Zufällige Reihenfolge" value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label="Nur Sperrbildschirm" value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div><div className="info"><h3>Ohne Bilderlimit</h3><p>Deine Auswahl wird privat auf dem Gerät gespeichert. Die einzige Grenze ist der freie Speicherplatz.</p></div></section> :
|
||||
<section className="gallery-page" aria-label="Meine Bilder">
|
||||
{gallery.length ? <div className="gallery-grid">{gallery.map((image, index) => <article className={image.selected ? "current" : ""} key={image.id}><img src={image.url} alt={`Bild ${index + 1}`} />{image.selected && <span className="current-badge"><Check /> Aktuell</span>}<button className="delete-button" aria-label={`Bild ${index + 1} löschen`} onClick={() => remove(image)} disabled={deletingId === image.id}><Trash2 /></button></article>)}</div> : !galleryLoading && <div className="gallery-empty"><Images /><h2>Deine Sammlung ist leer</h2><p>Füge Bilder hinzu, die automatisch als Sperrbildschirm wechseln sollen.</p><button className="primary" onClick={choose}><Plus /> Bilder hinzufügen</button></div>}
|
||||
{galleryLoading && <p className="gallery-status">Galerie wird geladen …</p>}
|
||||
{!galleryLoading && gallery.length < galleryTotal && <button className="load-more" onClick={() => loadGallery(gallery.length, true)}>Weitere Bilder laden</button>}
|
||||
</section>}
|
||||
</div>
|
||||
|
||||
{notice && <button className="snackbar" onClick={() => setNotice("")}>{notice}</button>}
|
||||
<nav><button className={tab === "home" ? "active" : ""} onClick={() => setTab("home")}><Home /><span>Start</span></button><button className={tab === "settings" ? "active" : ""} onClick={() => setTab("settings")}><Settings /><span>Einstellungen</span></button></nav>
|
||||
{tab !== "gallery" && <nav><button className={tab === "home" ? "active" : ""} onClick={() => setTab("home")}><Home /><span>Start</span></button><button className={tab === "settings" ? "active" : ""} onClick={() => setTab("settings")}><Settings /><span>Einstellungen</span></button></nav>}
|
||||
</main>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user