Salta ai contenuti

Tutorial 3: una scaletta con il pulsante GO in una UI web

Fai girare uno show con tre brani, lo vedi in una pagina web incorporata in una finestra e fai partire il brano successivo con un pulsante GO scritto in HTML e JavaScript. La pagina non tocca mai l’engine direttamente: invia comandi e legge topic attraverso il bridge della UI.

Tempo: circa 30 minuti. Ti servono JUCE 9.0.3 e il satellite shuffle_juce (tutorial 2), il cui JuceWebUiHost incorpora la pagina, e la webview della piattaforma: runtime e SDK di WebView2 su Windows, WKWebView su macOS, WebKitGTK su Linux. Nessun hardware audio: i brani suonano su un engine simulato.

flowchart LR
    page["page<br/>index.html, app.js"] -- "call transport.start_next_song" --> bridge["UI bridge"]
    bridge --> session["command session"] --> show["setlist::Show"]
    show -- "setlist.songs changed" --> bridge -- "snapshot, then deltas" --> page

Il programma è examples/juce/webui di shuffle_juce, compilato ed eseguito dalla sua CI. Configura shuffle_juce con JUCE (tutorial 1, passo 4), poi, nel checkout di shuffle_juce:

cmake --build --preset windows-msvc --target example_juce_webui

Eseguilo dall’albero di build di shuffle_juce (cmake-build/<preset>/examples/example_juce_webui_artefacts/...):

Come lo lanci Cosa succede
senza argomenti una finestra con la scaletta, GO, Next e Stop
--smoke la finestra si apre, il programma preme GO nella pagina ed esce quando il brano suona
--headless nessuna finestra: una pagina scriptata pilota lo stesso bridge (è quello che esegue la CI; funziona senza webview)

Premi GO: parte “Tidewater” e la lista lo segna come in riproduzione. Anche Next e Stop sono comandi. Quando la sessione ha bisogno che una persona confermi una richiesta durante uno show in corso, la pagina mostra la conferma in attesa (dal topic session) in una finestra di dialogo.

Un dominio mostra il suo stato alle pagine attraverso una TopicSource: un descrittore costante con uno JSON Schema e una read() che restituisce il valore attuale. SetlistTopics dell’esempio restituisce i brani come collezione con chiavi (un oggetto indicizzato per identificatore, più un array order), così una modifica costa un delta piccolo:

constexpr ui_bridge::TopicDescriptor kSetlistTopics[] {
{ .name = "setlist.songs", .summary = "The songs of the set list in order, the current one and whether it plays.",
.schema = kSongsSchema, .maxRate = 30.0 },
};

Chiama listener_->topicChanged ("setlist.songs") quando lo show segnala un evento; il bridge ripubblica anche dopo ogni comando applicato.

Tutto tranne la vista, nell’ordine in cui viene costruito (la struct Engine dell’esempio): i fake dal lato engine, lo setlist::Show, il catalogo e la sessione dei comandi, i topic, il tema, il bridge.

commands::Catalogue catalogue = std::move (*commands::buildCatalogue ({ setlist::setlistCommands(), commands::sessionCommands() }).value);
commands::CommandSession session { catalogue, show, clock };
SetlistTopics topics { show };
ui_bridge::ThemeSource theme { stageDark() }; // the ui.theme topic: CSS custom properties
ui_bridge::TopicSource* sources[] { &topics, &theme };
registry = std::move (*ui_bridge::buildTopicRegistry (sources).value);
bridge = std::make_unique<ui_bridge::Bridge> (session, registry, clock,
ui_bridge::BridgeSettings { .serverName = "examples/juce/webui", .serverVersion = "1.0.0" });

Poi la vista e il collegamento con la pagina:

juce_adapter::JuceTaskQueue tasks;
juce_adapter::JuceWebUiHost view; // a juce::Component
ui_bridge::WebUiPage page (*app.bridge, view, tasks); // one bridge connection per page load
page.load (app.content()); // the packaged files plus the generated client

Un timer di controllo chiama bridge->tick() più o meno una volta per fotogramma del display.

page/index.html contiene una lista e tre pulsanti; page/app.js si connette e li collega. Due file non si scrivono a mano: shuffle_bridge.js è il runtime fisso (core/ui_bridge/web/), e show_api.js viene generato dal catalogo dell’applicazione stessa all’avvio:

import { BridgeClient, LiveRegions, accessibleName, bindTheme, inProcessTransport } from "./shuffle_bridge.js";
import { catalogueEntries, catalogueHash, commands, createApi, subscribeTopic } from "./show_api.js";
const client = new BridgeClient (inProcessTransport (), {
name: "Set list page", version: "1.0",
capabilities: ["plans", "confirm", "events", "theme"],
catalogueHash, catalogueEntries,
});
const api = createApi (client);
$("go").addEventListener ("click", () => run (() => api.transport.start_next_song ({})));
await client.connect ();
bindTheme (client);
subscribeTopic (client, "setlist.songs", {}, renderSongs);

api.transport.start_next_song è il nome stesso del comando. Un comando rifiutato non è un’eccezione: è un risultato con outcome.status === "rejected" e problemi che dicono cosa correggere, che l’esempio mostra in una riga di stato.

La pagina è servita con una Content Security Policy rigorosa: niente <script> inline e niente attributi style=; script e stili vanno in file separati.

In un progetto generato con BACKEND juce che aggiunge shuffle_juce (tutorial 2), copia main.cpp e la cartella page/ dall’esempio, poi:

shuffle_engine_add_app (MyShow BACKEND juce GUI SOURCES src/main.cpp
USES juce_platform juce_webui ui_bridge setlist)
juce_add_binary_data (MyShowPage HEADER_NAME WebUiPageData.h NAMESPACE WebUiPageData
SOURCES page/index.html page/style.css page/app.js
<path to shuffle_engine>/core/ui_bridge/web/shuffle_bridge.js)
target_link_libraries (MyShow PRIVATE MyShowPage)

Configura come nel tutorial 2 (shuffle_juce aggiunto dopo shuffle_engine). Su Windows l’SDK di WebView2 deve essere trovato in fase di build (JUCE_WEBVIEW2_PACKAGE_LOCATION, vedi adapters/juce in shuffle_juce); senza, il programma si compila e la vista segnala che non è disponibile (l’esempio allora ripiega su --headless).

Prova una modifica: aggiungi un quarto brano nel costruttore di Engine (sia in factory.addSong sia nella lista di show.open), ricompila e guardalo comparire nella pagina senza toccare il JavaScript.

  • la pagina usa gli stessi comandi di ogni altra sorgente, con la stessa validazione, la stessa conferma e lo stesso annullamento;
  • lo stato arriva alla pagina come topic: prima uno snapshot, poi piccoli delta;
  • il client è generato dal catalogo, quindi pagina e applicazione non possono andare fuori sincrono.

Per saperne di più: Build a web UI in 10 minutes (dichiarazioni TypeScript, stream per i meter, accessibilità), Read state, Theme your UI. Le pagine in un browser separato, telefono compreso, si connettono con il trasporto WebSocket (shuffle_net_websocket, esempio ui_bridge/websocket); un binding WebAssembly è in arrivo (core/ui_bridge).