From 65f20652fc5ae7f264c003965a3c7003afadc2de Mon Sep 17 00:00:00 2001 From: Adam Laycock Date: Mon, 23 Mar 2026 20:23:03 +0000 Subject: [PATCH 1/3] feat: re-implement action to broadcast using sequence builder --- app/lib/sequence-builder.tsx | 46 +++++++++++++++++++ app/locales/en.ts | 5 +- app/routes/actions.$action._index.tsx | 19 ++++---- app/routes/actions.$action.edit.tsx | 32 +++++-------- app/routes/actions.add.tsx | 18 +------- app/routes/hook.$hook.tsx | 2 +- .../migration.sql | 17 +++++++ prisma/schema.prisma | 1 + 8 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 prisma/migrations/20260323193952_add_data_to_action/migration.sql diff --git a/app/lib/sequence-builder.tsx b/app/lib/sequence-builder.tsx index bdd8f92..67dbc7b 100644 --- a/app/lib/sequence-builder.tsx +++ b/app/lib/sequence-builder.tsx @@ -130,3 +130,49 @@ export const SequenceBuilder = ({ ) } + +export const SequenceViewer = ({ + queue, + sounds, + label +}: { + queue: string[] + sounds: Audio[] + label: string +}) => { + const {t} = useTranslation() + + let duration = 0 + + return ( +
+ {label} +
+ {queue.map((queuedId, i) => { + const sound = sounds.filter(({id}) => { + return id === queuedId + })[0] + + duration += sound.duration + + return ( +
+

{sound.name}

+

+ {getSecondsAsTime(sound.duration)} +

+
+ ) + })} +
+ {t('broadcast.builder.totalDuration', { + duration: getSecondsAsTime(duration) + })} +
+
+
+ ) +} diff --git a/app/locales/en.ts b/app/locales/en.ts index cbca71d..ba606d3 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -53,7 +53,9 @@ export const en = { 'An emoji to use as the action icon. Note that emoji render differently on the RPi screen.', 'actions.form.type.label': 'Type', 'actions.form.type.helper': - 'Broadcast runs a broadcast to the supplied zone. Lockdown toggles a system wide lockdown.', + 'Broadcast runs a broadcast to the supplied zone, you can build the broadcaast once the action has been created. Lockdown toggles a system wide lockdown.', + 'actions.form.sequence.label': 'Sequence', + 'actions.form.sequence.helper': 'Build your broadcast sequence.', 'actions.form.sound.label': 'Sound', 'actions.form.sound.helper': 'Which sound should be used when broadcasting?', 'button.cancel': 'Cancel', @@ -66,6 +68,7 @@ export const en = { 'actions.detail.sound': 'Sound:', 'actions.edit.metaTitle': 'Edit {{name}}', 'actions.edit.pageTitle': 'Edit {{name}}', + 'actions.detail.sequence.label': 'Sequence', 'backup.pageTitle': 'Backups', 'backup.create': 'Create Backup', 'broadcast.pageTitle': 'Broadcast', diff --git a/app/routes/actions.$action._index.tsx b/app/routes/actions.$action._index.tsx index 33c25f6..c267f76 100644 --- a/app/routes/actions.$action._index.tsx +++ b/app/routes/actions.$action._index.tsx @@ -12,6 +12,7 @@ import {Page, Actions} from '~/lib/ui' import {useTranslation} from '~/lib/i18n' import {translate} from '~/lib/i18n.shared' import {getRootI18n} from '~/lib/i18n.meta' +import {SequenceViewer} from '~/lib/sequence-builder' export const meta: MetaFunction = ({matches}) => { const {messages} = getRootI18n(matches) @@ -28,15 +29,16 @@ export const loader = async ({request, params}: LoaderFunctionArgs) => { const prisma = getPrisma() const action = await prisma.action.findFirstOrThrow({ - where: {id: params.action}, - include: {audio: true} + where: {id: params.action} }) - return {action} + const sounds = await prisma.audio.findMany({orderBy: {name: 'asc'}}) + + return {action, sounds} } const Action = () => { - const {action} = useLoaderData() + const {action, sounds} = useLoaderData() const navigate = useNavigate() const {t} = useTranslation() const typeLabels: Record = { @@ -54,11 +56,12 @@ const Action = () => { {t('actions.detail.type')}:{' '} {typeLabels[action.action] ?? action.action}

-

- {t('actions.detail.sound')}{' '} - {action.audio!.name} -

+ = ({matches, data}) => { const {messages} = getRootI18n(matches) @@ -61,16 +62,16 @@ export const action = async ({params, request}: ActionFunctionArgs) => { const name = formData.get('name') as string | undefined const icon = formData.get('icon') as string | undefined const action = formData.get('action') as string | undefined - const sound = formData.get('sound') as string | undefined + const data = formData.get('data') as string | undefined invariant(name) invariant(icon) invariant(action) - invariant(sound) + invariant(data) await prisma.action.update({ where: {id: params.action}, - data: {name, icon, action, audioId: sound} + data: {name, icon, action, data} }) return redirect(`/actions/${params.action}`) @@ -117,24 +118,13 @@ const AddAction = () => { - - - + { const name = formData.get('name') as string | undefined const icon = formData.get('icon') as string | undefined const action = formData.get('action') as string | undefined - const sound = formData.get('sound') as string | undefined invariant(name) invariant(icon) invariant(action) - invariant(sound) const newAction = await prisma.action.create({ - data: {name, icon, action, audioId: sound} + data: {name, icon, action} }) void trigger(`New Action: ${name}`, 'newAction') @@ -101,20 +99,6 @@ const AddAction = () => { - - - { } if (webhook.action.audioId) { - await broadcast(zone, webhook.action.audioId) + await broadcast(zone, webhook.action.data) } break case 'lockdown': diff --git a/prisma/migrations/20260323193952_add_data_to_action/migration.sql b/prisma/migrations/20260323193952_add_data_to_action/migration.sql new file mode 100644 index 0000000..6568213 --- /dev/null +++ b/prisma/migrations/20260323193952_add_data_to_action/migration.sql @@ -0,0 +1,17 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Action" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "icon" TEXT NOT NULL, + "action" TEXT NOT NULL, + "data" TEXT NOT NULL DEFAULT '', + "audioId" TEXT, + CONSTRAINT "Action_audioId_fkey" FOREIGN KEY ("audioId") REFERENCES "Audio" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Action" ("action", "audioId", "icon", "id", "name") SELECT "action", "audioId", "icon", "id", "name" FROM "Action"; +DROP TABLE "Action"; +ALTER TABLE "new_Action" RENAME TO "Action"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fcfdc5a..b992724 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -106,6 +106,7 @@ model Action { name String icon String action String + data String @default("") audio Audio? @relation(fields: [audioId], references: [id]) audioId String? From 14ad621d45e542bb74c560d5c8e6a6dc98a438ac Mon Sep 17 00:00:00 2001 From: AML - A Laycock Date: Tue, 24 Mar 2026 09:03:56 +0000 Subject: [PATCH 2/3] feat: use db seed to take the single audio id and convert it to a sequence --- prisma/seed.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/prisma/seed.js b/prisma/seed.js index c44dba2..d20836d 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -74,6 +74,19 @@ const main = async () => { await Promise.all(promises) } + + const actionsWithAudioIdAndNoData = await prisma.action.findMany({ + where: {audioId: {not: ''}, data: ''} + }) + + await Promise.all( + actionsWithAudioIdAndNoData.map(action => { + return prisma.action.update({ + where: {id: action.id}, + data: {data: `["${action.audioId}"]`} + }) + }) + ) } main() From 86ea347d9195e434abae8ea405cd5b2f6823c31c Mon Sep 17 00:00:00 2001 From: AML - A Laycock Date: Tue, 24 Mar 2026 10:58:52 +0000 Subject: [PATCH 3/3] docs: note that relation can be removed in future version --- prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b992724..14b37e0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -108,6 +108,7 @@ model Action { action String data String @default("") + // BREAKING CHANGE When we make the jump to 2.x this can be removed. Not Worth a full V2 bump just to take it out. audio Audio? @relation(fields: [audioId], references: [id]) audioId String?