Переработан дизайн, добавлена фича SoundLibrary
Deploy / build-and-deploy (push) Successful in 1m14s
Deploy / build-and-deploy (push) Successful in 1m14s
This commit is contained in:
+124
-34
@@ -23,6 +23,11 @@ type Campaign = {
|
||||
gridSize: number;
|
||||
};
|
||||
|
||||
type LibrarySound = {
|
||||
filename: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
function EmojiPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -73,6 +78,8 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
const [editSound, setEditSound] = useState("horn");
|
||||
const [editSoundUrl, setEditSoundUrl] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [librarySounds, setLibrarySounds] = useState<LibrarySound[]>([]);
|
||||
const [showLibrary, setShowLibrary] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const formRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -86,6 +93,16 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
|
||||
useEffect(() => { fetchItems(); }, [campaign.id]);
|
||||
|
||||
const fetchLibrary = async () => {
|
||||
const res = await fetch("/api/sounds");
|
||||
const data = await res.json();
|
||||
setLibrarySounds(data.sounds || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editSound === "custom") fetchLibrary();
|
||||
}, [editSound]);
|
||||
|
||||
const updateItem = async (item: Item) => {
|
||||
await fetch(`/api/campaigns/${campaign.id}/items`, {
|
||||
method: "PUT",
|
||||
@@ -176,6 +193,15 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
const data = await res.json();
|
||||
if (data.url) setEditSoundUrl(data.url);
|
||||
setUploading(false);
|
||||
fetchLibrary();
|
||||
};
|
||||
|
||||
const deleteSound = async (filename: string) => {
|
||||
await fetch(`/api/sounds?filename=${encodeURIComponent(filename)}`, { method: "DELETE" });
|
||||
if (librarySounds.find(s => s.filename === filename)?.url === editSoundUrl) {
|
||||
setEditSoundUrl(null);
|
||||
}
|
||||
fetchLibrary();
|
||||
};
|
||||
|
||||
const playSoundOnce = (url: string) => {
|
||||
@@ -233,42 +259,108 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{editSound === "custom" && (
|
||||
<div>
|
||||
<label className="text-[9px] font-mono text-slate-600 uppercase">File</label>
|
||||
<div className="flex gap-1 items-center">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".ogg"
|
||||
className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) uploadSound(f); }}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="font-mono text-[9px] h-10"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? "..." : editSoundUrl ? "✓" : "OGG"}
|
||||
</Button>
|
||||
{editSoundUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => playSoundOnce(editSoundUrl)}
|
||||
className="text-sm cursor-pointer hover:text-cyan-300"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button size="sm" onClick={submitItem} disabled={!editText} className="font-mono text-xs">
|
||||
{editItemId ? "SAVE" : "ADD"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{editSound === "custom" && (
|
||||
<div className="border-t border-slate-700/20 pt-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".ogg"
|
||||
className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) uploadSound(f); }}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="font-mono text-[10px]"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? "Uploading..." : "+ Upload OGG"}
|
||||
</Button>
|
||||
{editSoundUrl && (
|
||||
<div className="flex items-center gap-1 text-[10px] font-mono text-cyan-400">
|
||||
<span>✓ sound loaded</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => playSoundOnce(editSoundUrl)}
|
||||
className="px-1.5 py-0.5 rounded bg-slate-700/50 hover:bg-slate-600/50 text-xs cursor-pointer"
|
||||
title="Preview"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditSoundUrl(null)}
|
||||
className="px-1.5 py-0.5 rounded bg-slate-700/50 hover:bg-red-800/50 text-xs cursor-pointer"
|
||||
title="Clear"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLibrary(!showLibrary)}
|
||||
className="text-[10px] font-mono text-slate-500 hover:text-slate-300 cursor-pointer"
|
||||
>
|
||||
{showLibrary ? "▾ Hide Library" : "▸ Sound Library (" + librarySounds.length + ")"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showLibrary && librarySounds.length > 0 && (
|
||||
<div className="max-h-40 overflow-y-auto space-y-1 border border-slate-700/20 rounded p-2 bg-slate-900/40">
|
||||
{librarySounds.map(s => {
|
||||
const isSelected = editSoundUrl === s.url;
|
||||
return (
|
||||
<div
|
||||
key={s.filename}
|
||||
className={"flex items-center gap-1 px-2 py-1 rounded text-[10px] font-mono " + (isSelected ? "bg-cyan-900/40 text-cyan-300" : "text-slate-400 hover:bg-slate-800/40")}
|
||||
>
|
||||
<span className="flex-1 truncate">{s.filename}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => playSoundOnce(s.url)}
|
||||
className="px-1.5 py-0.5 rounded hover:bg-slate-700/50 cursor-pointer"
|
||||
title="Play"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditSoundUrl(s.url)}
|
||||
className="px-1.5 py-0.5 rounded hover:bg-slate-700/50 cursor-pointer"
|
||||
title="Use this sound"
|
||||
>
|
||||
{isSelected ? "✓" : "Use"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteSound(s.filename)}
|
||||
className="px-1.5 py-0.5 rounded hover:bg-red-800/50 cursor-pointer text-red-400"
|
||||
title="Delete"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLibrary && librarySounds.length === 0 && (
|
||||
<p className="text-[9px] font-mono text-slate-600 italic">No uploaded sounds yet. Upload an OGG file above.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -369,5 +461,3 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user