diff --git a/README.md b/README.md index 84e26ca..b4bf0e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # The Twist > [!IMPORTANT] -> This repository is still in the prototype stage with many of The Twist's components, resources, and documentation being WIPs. **Nevertheless, leaving a star (⭐) helps the project a lot and keeps you in the loop!** +> This repository is still in the prototype stage with many of The Twist's components, resources, and documentation being WIP. **Nevertheless, leaving a star (⭐) helps the project and keeps you in the loop!** 🌀 The Bender Twist is a [free](https://en.wikipedia.org/wiki/Free_software), portable platform which augments the functional capabilities of LT-series amps made by a certain guitar brand that rhymes with "bender" and names products after horses. diff --git a/backend/app.py b/backend/app.py index b555a11..aac5cf9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -64,6 +64,7 @@ class DeviceStore: self.audition_status: str = '' self.firmware_version: str = '' self.memory_usage: dict = {} + self.presets: List[dict] = [] self.processor_usage: dict = {} async def broadcast(self): @@ -77,6 +78,7 @@ class DeviceStore: "firmware_version": self.firmware_version, "memory_usage": self.memory_usage, "processor_usage": self.processor_usage, + "presets": self.presets, } for ws in self.connections.copy(): try: @@ -90,6 +92,17 @@ class DeviceStore: self.details = details await self.broadcast() + + async def load_presets(self): + preset_idx = 1 + preset = True + presets = [] + while preset and preset_idx <= 80: + preset = self.amp.retrieve_preset(preset_idx) + if preset and "data" in preset and preset["index"] == preset_idx: + presets.append({"name": json.loads(preset["data"])["info"]["displayName"], "index": preset["index"]}) + preset_idx += 1 + device_store.presets = presets async def update_data(self): if self.amp: @@ -115,10 +128,11 @@ def on_startup(): ## device api (mainly websockets) async def heartbeat_loop(): + asyncio.create_task(device_store.load_presets()) while device_store.status == "online": try: if device_store.amp: - asyncio.to_thread(device_store.amp.send_heartbeat) + await asyncio.to_thread(device_store.amp.send_heartbeat) product = await asyncio.to_thread(device_store.amp.request_product_id) preset = await asyncio.to_thread(device_store.amp.request_current_preset) fw = await asyncio.to_thread(device_store.amp.request_firmware_version) @@ -126,7 +140,6 @@ async def heartbeat_loop(): memory = await asyncio.to_thread(device_store.amp.request_memory_usage) proc = await asyncio.to_thread(device_store.amp.request_processor_utilization) - changed = False device_store.product_name = product device_store.current_preset = preset device_store.audition_status = audition @@ -139,8 +152,8 @@ async def heartbeat_loop(): await device_store.set_status("warning", {"error": str(e)}) await device_store.broadcast() device_store.status = "offline" - device_store.update_data() - + await device_store.update_data() + async def handle_ws_action(msg): action = msg.get("action") @@ -248,9 +261,11 @@ class Playlist(PlaylistBase, table=True): last_updated: Optional[datetime] = Field(default_factory=datetime.utcnow) class PlaylistItem(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int = Field(default=None, primary_key=True) playlist_id: int = Field(foreign_key="playlist.id") preset_number: int # 1-60 + note: Optional[str] = None + position: int class PlaylistOut(PlaylistBase): id: int @@ -318,17 +333,25 @@ def get_playlist(playlist_id: int): } @app.post("/playlists/{playlist_id}/items/", response_model=List[PlaylistItem]) -def add_items(playlist_id: int, preset_numbers: List[int]): +def add_items(playlist_id: int, preset_numbers: List[int], notes: List[str]): + print(playlist_id) for n in preset_numbers: if not (1 <= n <= 60): raise HTTPException(status_code=400, detail="Preset numbers must be between 1 and 60") with Session(engine) as session: playlist = session.get(Playlist, playlist_id) + print(playlist, playlist_id) if not playlist: raise HTTPException(status_code=404, detail="Playlist not found") items = [] - for number in preset_numbers: - item = PlaylistItem(playlist_id=playlist_id, preset_number=number) + for number in range(len(preset_numbers)): + max_position = session.exec( + select(func.max(PlaylistItem.position)).where(PlaylistItem.playlist_id == playlist_id) + ).one() + if max_position is None: + max_position = 0 + + item = PlaylistItem(playlist_id=playlist_id, preset_number=preset_numbers[number], note=notes[number], position=max_position + 1) session.add(item) items.append(item) playlist.last_updated = datetime.utcnow() @@ -338,6 +361,86 @@ def add_items(playlist_id: int, preset_numbers: List[int]): session.refresh(item) return items +@app.post("/playlists/{playlist_id}/items/insert", response_model=List[PlaylistItem]) +def insert_item(playlist_id: int, preset_number: int = Body(...), position: int = Body(...)): + if not (1 <= preset_number <= 60): + raise HTTPException(status_code=400, detail="Preset number must be between 1 and 60") + with Session(engine) as session: + playlist = session.get(Playlist, playlist_id) + if not playlist: + raise HTTPException(status_code=404, detail="Playlist not found") + items = session.exec( + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id).order_by(PlaylistItem.position) + ).all() + + for item in items: + if item.position >= position: + item.position += 1 + session.add(item) + + new_item = PlaylistItem(playlist_id=playlist_id, preset_number=preset_number, position=position) + session.add(new_item) + playlist.last_updated = datetime.utcnow() + session.add(playlist) + session.commit() + + items = session.exec( + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id).order_by(PlaylistItem.position) + ).all() + return items + +@app.delete("/playlists/{playlist_id}/items/{item_id}", response_model=dict) +def delete_item(playlist_id: int, item_id: int): + with Session(engine) as session: + item = session.get(PlaylistItem, item_id) + if not item or item.playlist_id != playlist_id: + raise HTTPException(status_code=404, detail="Playlist item not found") + position = item.position + session.delete(item) + + items = session.exec( + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id) + ).all() + for i in items: + if i.position > position: + i.position -= 1 + session.add(i) + playlist = session.get(Playlist, playlist_id) + playlist.last_updated = datetime.utcnow() + session.add(playlist) + session.commit() + return {"ok": True} + +@app.patch("/playlists/{playlist_id}/items/{item_id}/move", response_model=List[PlaylistItem]) +def move_item(playlist_id: int, item_id: int, new_position: int = Body(...)): + with Session(engine) as session: + items = session.exec( + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id).order_by(PlaylistItem.position) + ).all() + item = session.get(PlaylistItem, item_id) + if not item or item.playlist_id != playlist_id: + raise HTTPException(status_code=404, detail="Playlist item not found") + old_position = item.position + if new_position < 0 or new_position >= len(items): + raise HTTPException(status_code=400, detail="Invalid new position") + + items.remove(item) + + items.insert(new_position, item) + + for idx, itm in enumerate(items): + itm.position = idx + session.add(itm) + playlist = session.get(Playlist, playlist_id) + playlist.last_updated = datetime.utcnow() + session.add(playlist) + session.commit() + + items = session.exec( + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id).order_by(PlaylistItem.position) + ).all() + return items + @app.patch("/playlists/{playlist_id}", response_model=PlaylistOut) def update_playlist( playlist_id: int, @@ -365,16 +468,17 @@ def update_playlist( size=size, ) -@app.get("/playlists/{playlist_id}/items/", response_model=List[int]) +@app.get("/playlists/{playlist_id}/items", response_model=List[PlaylistItem]) def list_items(playlist_id: int): with Session(engine) as session: playlist = session.get(Playlist, playlist_id) + print(playlist) if not playlist: raise HTTPException(status_code=404, detail="Playlist not found") items = session.exec( - select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id) + select(PlaylistItem).where(PlaylistItem.playlist_id == playlist_id).order_by(PlaylistItem.position) ).all() - return [item.preset_number for item in items] + return items @app.delete("/playlists/{playlist_id}") def delete_playlist(playlist_id: int): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b879392..dde08fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "the-twist-frontend", "version": "0.1.0", "dependencies": { + "@dnd-kit-svelte/core": "^0.0.11", + "@dnd-kit-svelte/sortable": "^0.0.11", + "@dnd-kit-svelte/utilities": "^0.0.11", "@lucide/svelte": "^0.525.0", "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", @@ -37,6 +40,58 @@ "node": ">=6.0.0" } }, + "node_modules/@dnd-kit-svelte/accessibility": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/accessibility/-/accessibility-0.0.11.tgz", + "integrity": "sha512-YkMjzabpnwELyeOSC2COV6EIGWZM8m4kgi/wIMwG0g/ZO3WWuzkMiLRLhYZqRkrEdpoz5tZnVclw+x/kcUjQHw==", + "dependencies": { + "@dnd-kit-svelte/utilities": "latest", + "esm-env": "^1.2.2", + "runed": "^0.23.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@dnd-kit-svelte/core": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/core/-/core-0.0.11.tgz", + "integrity": "sha512-bVqutZhSOk3nQsjwJ0SveUrPeBgUO/wajfon1AiKA8mxfQhaUG97CfoQLs7tLW3YxSkHkLPhYRw9WQat7OMB7g==", + "dependencies": { + "@dnd-kit-svelte/accessibility": "latest", + "@dnd-kit-svelte/utilities": "latest", + "runed": "^0.23.0", + "svelte-toolbelt": "^0.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@dnd-kit-svelte/sortable": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/sortable/-/sortable-0.0.11.tgz", + "integrity": "sha512-QeFvpjR9KAIVFs0/S4kGtzSNZzEmIp+d8DiNCRUeVC2E4Z5P4INzdzJl7m1EM7UjvdNayfLWcMhLpyTIHqHxQQ==", + "dependencies": { + "@dnd-kit-svelte/core": "latest", + "@dnd-kit-svelte/utilities": "latest", + "runed": "^0.23.0", + "svelte-toolbelt": "^0.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@dnd-kit-svelte/utilities": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/utilities/-/utilities-0.0.11.tgz", + "integrity": "sha512-Y9Z016gLdieHiAVoteSBa1tvrgug1/Rl5wdKw/2g1Fd4G0q9HWIyPe2evKrv8H5ePfcNegA1jN7fneDVmHd3pQ==", + "dependencies": { + "svelte-toolbelt": "^0.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -1658,6 +1713,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -2089,6 +2149,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2097,6 +2172,14 @@ "node": ">=0.10.0" } }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/svelte": { "version": "5.35.2", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.2.tgz", @@ -2132,6 +2215,26 @@ "url": "https://github.com/sponsors/ItalyPaleAle" } }, + "node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -2296,6 +2399,46 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "@dnd-kit-svelte/accessibility": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/accessibility/-/accessibility-0.0.11.tgz", + "integrity": "sha512-YkMjzabpnwELyeOSC2COV6EIGWZM8m4kgi/wIMwG0g/ZO3WWuzkMiLRLhYZqRkrEdpoz5tZnVclw+x/kcUjQHw==", + "requires": { + "@dnd-kit-svelte/utilities": "latest", + "esm-env": "^1.2.2", + "runed": "^0.23.0" + } + }, + "@dnd-kit-svelte/core": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/core/-/core-0.0.11.tgz", + "integrity": "sha512-bVqutZhSOk3nQsjwJ0SveUrPeBgUO/wajfon1AiKA8mxfQhaUG97CfoQLs7tLW3YxSkHkLPhYRw9WQat7OMB7g==", + "requires": { + "@dnd-kit-svelte/accessibility": "latest", + "@dnd-kit-svelte/utilities": "latest", + "runed": "^0.23.0", + "svelte-toolbelt": "^0.7.0" + } + }, + "@dnd-kit-svelte/sortable": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/sortable/-/sortable-0.0.11.tgz", + "integrity": "sha512-QeFvpjR9KAIVFs0/S4kGtzSNZzEmIp+d8DiNCRUeVC2E4Z5P4INzdzJl7m1EM7UjvdNayfLWcMhLpyTIHqHxQQ==", + "requires": { + "@dnd-kit-svelte/core": "latest", + "@dnd-kit-svelte/utilities": "latest", + "runed": "^0.23.0", + "svelte-toolbelt": "^0.7.0" + } + }, + "@dnd-kit-svelte/utilities": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dnd-kit-svelte/utilities/-/utilities-0.0.11.tgz", + "integrity": "sha512-Y9Z016gLdieHiAVoteSBa1tvrgug1/Rl5wdKw/2g1Fd4G0q9HWIyPe2evKrv8H5ePfcNegA1jN7fneDVmHd3pQ==", + "requires": { + "svelte-toolbelt": "^0.7.0" + } + }, "@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -3354,6 +3497,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, "is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -3566,11 +3714,27 @@ "fsevents": "~2.3.2" } }, + "runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "requires": { + "esm-env": "^1.0.0" + } + }, "source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, + "style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "requires": { + "inline-style-parser": "0.2.4" + } + }, "svelte": { "version": "5.35.2", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.2.tgz", @@ -3600,6 +3764,16 @@ "regexparam": "2.0.2" } }, + "svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "requires": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + } + }, "tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5bf1989..18affac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,9 @@ "vite": "^7.0.2" }, "dependencies": { + "@dnd-kit-svelte/core": "^0.0.11", + "@dnd-kit-svelte/sortable": "^0.0.11", + "@dnd-kit-svelte/utilities": "^0.0.11", "@lucide/svelte": "^0.525.0", "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", diff --git a/frontend/src/Layout.svelte b/frontend/src/Layout.svelte index 88e6517..1a04ee1 100644 --- a/frontend/src/Layout.svelte +++ b/frontend/src/Layout.svelte @@ -26,6 +26,8 @@ import IconCheckCircle from '@lucide/svelte/icons/check-circle'; import IconAlertTriangle from '@lucide/svelte/icons/alert-triangle'; import IconXCircle from '@lucide/svelte/icons/x-circle'; + import IconListVideo from '@lucide/svelte/icons/list-video'; + import IconGlobe from '@lucide/svelte/icons/globe'; // setup/handle device websockets import { onMount } from 'svelte'; @@ -40,11 +42,14 @@ firmwareVersion, playlists, playlistsLoading, + playlistItems, + playlistItemsLoading, creatingPlaylist, - showPlaylistModal + showPlaylistModal, + allPresets, + presetsLoading } from './stores/state.js'; - let syncing = $state(false); let oldDeviceStatus = $state('offline'); function waitForSync() { @@ -132,6 +137,10 @@ memoryUsage.set(msg.memory_usage ?? memoryUsage); processorUsage.set(msg.processor_usage ?? processorUsage); firmwareVersion.set(msg.firmware_version ?? firmwareVersion); + allPresets.set(msg.presets ?? allPresets); + if ($allPresets && $allPresets.length > 0) { + presetsLoading.set(false); + } } } catch (e) { toaster.error({ @@ -164,10 +173,9 @@ title: 'Error fetching playlists', description: e.message }); - } finally { - playlistsLoading.set(false); } } + async function createPlaylist(newName, newDescription) { creatingPlaylist.set(true); try { @@ -198,10 +206,35 @@ } } + async function refreshPlaylistItems() { + playlistItemsLoading.set(true); + try { + const res = await fetch( + `http://${location.host}/playlists/${$routerLocation.split('/')[2]}/items` + ); + console.log(res); + if (!res.ok) throw new Error('Failed to fetch playlist items'); + playlistItems.set(await res.json()); + } catch (e) { + toaster.error({ + title: 'Error fetching playlist items', + description: e.message + }); + } finally { + playlistItemsLoading.set(false); + } + } + + $effect(async () => { + if ($routerLocation.split('/').includes('playlist')) { + console.log($routerLocation); + await refreshPlaylistItems(); + } + }); + onMount(() => { connectDeviceWS(); fetchPlaylists(); - return () => wsDevice && wsDevice.close(); }); @@ -238,6 +271,10 @@ description: 'Name and description are required to create a playlist.' }); } + } else if ($layoutEvents.type === 'refetch_playlist_items') { + if ($routerLocation.split('/').includes('playlist')) { + refreshPlaylistItems(); + } } layoutEvents.set(null); @@ -250,24 +287,51 @@
{#snippet lead()} - + {#if $routerLocation.split('/').includes('playlist')} + + + + {:else if $routerLocation != '/'} + + + + {:else} + + + + {/if} {/snippet} {#snippet trail()}
{/snippet} {#snippet headline()} -

{pageNames[$routerLocation] ?? 'Unknown'}

+

+ {#if $routerLocation + .split('/') + .includes('playlist') && $playlists && $playlists.length > 0} + {#if $playlists.find((p) => p.id == $routerLocation.split('/')[2])} +
+ + {$playlists.find((p) => p.id == $routerLocation.split('/')[2]).name} +
+ {:else} + Unknown Playlist + {/if} + {:else} + {pageNames[$routerLocation] ?? 'Unknown'} + {/if} +

{/snippet} Bender Logo
-
+
- +
diff --git a/frontend/src/components/DeviceStatusCard.svelte b/frontend/src/components/DeviceStatusCard.svelte index 247dce5..16fd202 100644 --- a/frontend/src/components/DeviceStatusCard.svelte +++ b/frontend/src/components/DeviceStatusCard.svelte @@ -68,6 +68,7 @@

Your amp connectivity status is currently:

+ {#if $productName || $firmwareVersion || ($processorUsage && $processorUsage['percent'])}
diff --git a/frontend/src/components/PlaylistCard.svelte b/frontend/src/components/PlaylistCard.svelte index b736ab1..c644409 100644 --- a/frontend/src/components/PlaylistCard.svelte +++ b/frontend/src/components/PlaylistCard.svelte @@ -1,7 +1,5 @@ + +
  • + +
    + {task.content} +
    + + + {#if isDragging.current} +
    + +
    + Moving: {task.content} +
    +
    + {/if} +
  • diff --git a/frontend/src/components/PresetList.svelte b/frontend/src/components/PresetList.svelte new file mode 100644 index 0000000..6cdb9c2 --- /dev/null +++ b/frontend/src/components/PresetList.svelte @@ -0,0 +1,107 @@ + + + +
    + {@render taskList('in-progress', 'In Progress', inProgress)} + {@render taskList('done', 'Done', done)} +
    + + + {#if activeTodo && activeId} + + {/if} + +
    + +{#snippet taskList(id: string, title: string, tasks: Todo[])} + + +

    {title}

    + +
    + {#each tasks as task (task.id)} +
      + +
    + {/each} +
    +
    +
    +{/snippet} diff --git a/frontend/src/lib/components/droppable.svelte b/frontend/src/lib/components/droppable.svelte new file mode 100644 index 0000000..c1de8dd --- /dev/null +++ b/frontend/src/lib/components/droppable.svelte @@ -0,0 +1,18 @@ + + +
    + {@render children()} +
    diff --git a/frontend/src/lib/dnd.js b/frontend/src/lib/dnd.js new file mode 100644 index 0000000..c99ee16 --- /dev/null +++ b/frontend/src/lib/dnd.js @@ -0,0 +1,20 @@ +import { + defaultDropAnimationSideEffects, + useSensors, + useSensor, + TouchSensor, + KeyboardSensor, + MouseSensor, +} from '@dnd-kit-svelte/core'; + +export const dropAnimation = { + sideEffects: defaultDropAnimationSideEffects({ + styles: { + active: { + opacity: '0.5', + }, + }, + }), +}; + +export const sensors = useSensors(useSensor(TouchSensor), useSensor(KeyboardSensor), useSensor(MouseSensor)); diff --git a/frontend/src/pages/Device.svelte b/frontend/src/pages/Device.svelte index 8ac2d61..58462ba 100644 --- a/frontend/src/pages/Device.svelte +++ b/frontend/src/pages/Device.svelte @@ -7,8 +7,10 @@ import DeviceControlCard from '../components/DeviceControlCard.svelte'; - +
    + -{#if $deviceStatus === 'online'} - -{/if} + {#if $deviceStatus === 'online'} + + {/if} +
    diff --git a/frontend/src/pages/Home.svelte b/frontend/src/pages/Home.svelte index 8c54fa8..944ab4e 100644 --- a/frontend/src/pages/Home.svelte +++ b/frontend/src/pages/Home.svelte @@ -4,8 +4,10 @@ import PlaylistsSection from '../components/HomePlaylistsSection.svelte'; - +
    + - + - + +
    diff --git a/frontend/src/pages/Playlist.svelte b/frontend/src/pages/Playlist.svelte new file mode 100644 index 0000000..9454089 --- /dev/null +++ b/frontend/src/pages/Playlist.svelte @@ -0,0 +1,147 @@ + + +
    + {#if playlist != null} +
    +

    + {playlist.description != '~' ? playlist.description : 'No description'} +

    +
    + + + {playlist.size} presets • + + Updated {formatDate(playlist.last_updated)} +
    +
    + + + +
    +
    +
    +

    Presets:

    + {#if $playlistItems && $playlistItems.length > 0} + {#each $playlistItems as item} +
    +
    +

    {item.preset_number}

    +
    +

    {$allPresets[item.preset_number - 1].name}

    +

    {item.note ? '• ' + item.note : ''}

    +
    + {/each} + {:else} +

    No presets in this playlist.

    + {/if} +
    +
    + +
    + + + +
    +
    + {:else} +
    + +
    +

    Error

    +

    Playlist not found.

    +
    +
    + Go Home +
    +
    + {/if} + +
    diff --git a/frontend/src/pages/Playlists.svelte b/frontend/src/pages/Playlists.svelte index d1e6893..fa39805 100644 --- a/frontend/src/pages/Playlists.svelte +++ b/frontend/src/pages/Playlists.svelte @@ -1,11 +1,28 @@ -
    - {#each $playlists as playlist} - - {/each} +
    +
    + {#each $playlists as playlist} + + {/each} +
    + +
    diff --git a/frontend/src/routes.js b/frontend/src/routes.js index 34b54f0..dd1da5a 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -1,9 +1,11 @@ import Home from './pages/Home.svelte'; import Device from './pages/Device.svelte'; import Playlists from './pages/Playlists.svelte'; +import Playlist from './pages/Playlist.svelte'; export default { '/': Home, '/device': Device, - '/playlists': Playlists + '/playlists': Playlists, + '/playlist/:id': Playlist }; diff --git a/frontend/src/stores/state.js b/frontend/src/stores/state.js index ab91830..5971a83 100644 --- a/frontend/src/stores/state.js +++ b/frontend/src/stores/state.js @@ -10,6 +10,10 @@ export const memoryUsage = writable(undefined); export const processorUsage = writable(undefined); export const firmwareVersion = writable(undefined); export const playlists = writable([]); +export const playlistItems = writable([]); export const playlistsLoading = writable(true); +export const playlistItemsLoading = writable(false); export const creatingPlaylist = writable(false); export const showPlaylistModal = writable(false); +export const allPresets = writable([]); +export const presetsLoading = writable(true);