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}
-
+
-
+
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'])}
{#if $productName}
diff --git a/frontend/src/components/HomeHero.svelte b/frontend/src/components/HomeHero.svelte
index 5f8b114..0acca91 100644
--- a/frontend/src/components/HomeHero.svelte
+++ b/frontend/src/components/HomeHero.svelte
@@ -15,7 +15,7 @@
title: 'Coming soon!',
description: 'When implemented, you will be able to dismiss this hero.'
})}
- class="absolute top-8 right-0"
+ class="absolute top-2 right-0"
/>
- {#if playlists.length == 0}
+ {#if $playlists.length == 0}
@@ -145,8 +145,10 @@
document
.querySelector('#playlist-row')
.scrollBy({ left: -200, behavior: 'smooth' });
- }}>
+
+
{
document.querySelector('#playlist-row').scrollBy({ left: 200, behavior: 'smooth' });
- }}>
+
+
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}
+
+
+
{$allPresets[item.preset_number - 1].name}
+
{item.note ? '• ' + item.note : ''}
+
+ {/each}
+ {:else}
+
No presets in this playlist.
+ {/if}
+
+
+
+
+
+
+
+
+
+ {:else}
+
+
+
+
Error
+
Playlist not found.
+
+
+
+ {/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);