feat(setup): bootstrap Android plugin and web UI
This initial commit adds a multi-part scaffold for Wechselbild, including a web UI and an Android plugin. It introduces frontend assets and a package.json-based dev setup, plus plugin scaffolding for Android with Kotlin sources and Cargo.toml for the Tauri plugin. - Adds Android wallpaper plugin with Kotlin sources and manifest - Includes web UI scaffold with index.html and package.json - Adds Tauri plugin scaffold with Cargo.toml and build config
This commit is contained in:
+70
@@ -0,0 +1,70 @@
|
||||
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";
|
||||
|
||||
const initial: WallpaperState = { imageCount: 0, enabled: false, shuffle: true, lockScreenOnly: true, currentIndex: 0, imageUrls: [] };
|
||||
|
||||
function Switch({ checked, onChange, label }: { checked: boolean; onChange: (value: boolean) => void; label: string }) {
|
||||
return <button className={`switch ${checked ? "on" : ""}`} role="switch" aria-checked={checked} aria-label={label} onClick={() => onChange(!checked)}><span /></button>;
|
||||
}
|
||||
|
||||
function SettingRow({ icon, label, value, onChange }: { icon: React.ReactNode; label: string; value: boolean; onChange: (value: boolean) => void }) {
|
||||
return <div className="setting-row"><div className="setting-icon">{icon}</div><span>{label}</span><Switch checked={value} onChange={onChange} label={label} /></div>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [state, setState] = useState(initial);
|
||||
const [tab, setTab] = useState<"home" | "settings">("home");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [notice, setNotice] = 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]);
|
||||
|
||||
async function update(name: "enabled" | "shuffle" | "lockScreenOnly", value: boolean) {
|
||||
setState(prev => ({ ...prev, [name]: value }));
|
||||
try { setState(await setSetting(name, value)); setNotice(name === "enabled" ? (value ? "Automatischer Wechsel ist aktiv" : "Automatischer Wechsel pausiert") : "Einstellung gespeichert"); } catch { setNotice("Diese Funktion ist auf Android verfügbar"); }
|
||||
}
|
||||
|
||||
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"); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
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) })); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
return <main className="app-shell">
|
||||
<header><div><h1>Wechselbild</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" ? <>
|
||||
<section className="hero" aria-label="Aktuelles Hintergrundbild">
|
||||
<div className="photo-stack" />
|
||||
<img src={current} alt="Aktuelles Motiv" />
|
||||
<div className="hero-shade" />
|
||||
<div className="hero-meta"><span><Sparkles size={16} /> Aktuelles Motiv</span><button onClick={next} disabled={busy}>Nächstes Motiv <ChevronRight size={18} /></button></div>
|
||||
</section>
|
||||
|
||||
<SettingRow icon={<Smartphone />} label="Bei jedem Aktivieren wechseln" value={state.enabled} onChange={v => update("enabled", v)} />
|
||||
|
||||
<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>
|
||||
|
||||
<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 Wechselbild 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>}
|
||||
</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>
|
||||
</main>;
|
||||
}
|
||||
Reference in New Issue
Block a user