Mercurial > code > home > repos > light9
comparison web/ascoltami/Light9AscoltamiUi.ts @ 2439:06da5db2fafe
rewrite ascoltami to use the graph for more playback data
author | drewp@bigasterisk.com |
---|---|
date | Thu, 30 May 2024 01:08:07 -0700 |
parents | 4556eebe5d73 |
children | 2ce77421c0b7 |
comparison
equal
deleted
inserted
replaced
2438:f2b3cfcc23d3 | 2439:06da5db2fafe |
---|---|
1 import debug from "debug"; | 1 import debug from "debug"; |
2 import { css, html, LitElement } from "lit"; | 2 import { css, html, LitElement, PropertyValues } from "lit"; |
3 import { customElement, property } from "lit/decorators.js"; | 3 import { customElement, property } from "lit/decorators.js"; |
4 import { classMap } from "lit/directives/class-map.js"; | |
5 import { NamedNode } from "n3"; | 4 import { NamedNode } from "n3"; |
6 import Sylvester from "sylvester"; | |
7 import { Zoom } from "../light9-timeline-audio"; | |
8 import { PlainViewState } from "../Light9CursorCanvas"; | 5 import { PlainViewState } from "../Light9CursorCanvas"; |
9 import { getTopGraph } from "../RdfdbSyncedGraph"; | 6 import { getTopGraph } from "../RdfdbSyncedGraph"; |
10 import { SyncedGraph } from "../SyncedGraph"; | 7 import { SyncedGraph } from "../SyncedGraph"; |
11 import { TimingUpdate } from "./main"; | 8 import { PlayerState } from "./PlayerState"; |
12 import { showRoot } from "../show_specific"; | |
13 export { Light9TimelineAudio } from "../light9-timeline-audio"; | |
14 export { Light9CursorCanvas } from "../Light9CursorCanvas"; | |
15 export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph"; | 9 export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph"; |
16 export { ResourceDisplay } from "../ResourceDisplay"; | 10 export { ResourceDisplay } from "../ResourceDisplay"; |
17 const $V = Sylvester.Vector.create; | 11 export { Light9AscoltamiTimeline } from "./Light9AscoltamiTimeline"; |
12 export { Light9SongListing } from "./Light9SongListing"; | |
18 | 13 |
19 debug.enable("*"); | 14 debug.enable("*"); |
20 const log = debug("asco"); | 15 const log = debug("asco"); |
21 | 16 |
22 function byId(id: string): HTMLElement { | |
23 return document.getElementById(id)!; | |
24 } | |
25 async function postJson(url: string, jsBody: Object) { | 17 async function postJson(url: string, jsBody: Object) { |
26 return fetch(url, { | 18 return fetch(url, { |
27 method: "POST", | 19 method: "POST", |
28 headers: { "Content-Type": "applcation/json" }, | 20 headers: { "Content-Type": "applcation/json" }, |
29 body: JSON.stringify(jsBody), | 21 body: JSON.stringify(jsBody), |
30 }); | 22 }); |
31 } | 23 } |
24 | |
32 @customElement("light9-ascoltami-ui") | 25 @customElement("light9-ascoltami-ui") |
33 export class Light9AscoltamiUi extends LitElement { | 26 export class Light9AscoltamiUi extends LitElement { |
34 graph!: SyncedGraph; | 27 graph!: SyncedGraph; |
35 times!: { intro: number; post: number }; | |
36 @property() nextText: string = ""; | |
37 @property() isPlaying: boolean = false; | |
38 @property() show: NamedNode | null = null; | 28 @property() show: NamedNode | null = null; |
39 @property() song: NamedNode | null = null; | 29 @property() song: NamedNode | null = null; |
40 @property() selectedSong: NamedNode | null = null; | 30 @property() selectedSong: NamedNode | null = null; |
41 @property() currentDuration: number = 0; | |
42 @property() zoom: Zoom; | |
43 @property() overviewZoom: Zoom; | |
44 @property() viewState: PlainViewState | null = null; | 31 @property() viewState: PlainViewState | null = null; |
32 @property() host: any; | |
33 @property() playerState: PlayerState = { duration: null, endOfSong: null, pausedSongTime: null, playing: null, song: null, wallStartTime: null }; | |
34 @property() playerTime: number = 0; | |
45 static styles = [ | 35 static styles = [ |
46 css` | 36 css` |
47 :host { | 37 :host { |
48 display: flex; | 38 display: flex; |
49 flex-direction: column; | 39 flex-direction: column; |
50 } | 40 } |
51 .timeRow { | 41 |
52 margin: 14px; | 42 .keyCap { |
53 position: relative; | 43 color: #ccc; |
54 } | 44 background: #525252; |
55 #overview { | 45 display: inline-block; |
56 height: 60px; | 46 border: 1px outset #b3b3b3; |
57 } | 47 padding: 2px 3px; |
58 #zoomed { | 48 margin: 3px 0; |
59 margin-top: 40px; | 49 margin-left: 0.4em; |
60 height: 80px; | 50 font-size: 16px; |
61 } | 51 box-shadow: 0.9px 0.9px 0px 2px #565656; |
62 #cursor { | 52 border-radius: 2px; |
63 position: absolute; | 53 } |
64 left: 0; | 54 |
65 top: 0; | 55 button { |
66 width: 100%; | 56 min-height: 48pt; |
67 height: 100%; | 57 min-width: 65pt; |
68 } | 58 } |
69 #grow { | 59 |
70 flex: 1 1 auto; | 60 #mainRow { |
71 display: flex; | 61 display: flex; |
72 } | 62 flex-direction: row; |
73 #grow > span { | 63 } |
74 display: flex; | 64 |
75 position: relative; | 65 light9-song-listing { |
76 width: 50%; | 66 flex-grow: 1; |
77 } | 67 } |
78 #playSelected { | 68 |
79 height: 100px; | 69 th { |
80 } | 70 text-align: right; |
81 #songList { | |
82 overflow-y: scroll; | |
83 position: absolute; | |
84 left: 0; | |
85 top: 0; | |
86 right: 0; | |
87 bottom: 0; | |
88 } | |
89 #songList .row { | |
90 width: 60%; | |
91 min-height: 40px; | |
92 text-align: left; | |
93 position: relative; | |
94 } | |
95 #songList .row:nth-child(even) { | |
96 background: #333; | |
97 } | |
98 #songList .row:nth-child(odd) { | |
99 background: #444; | |
100 } | |
101 #songList button { | |
102 min-height: 40px; | |
103 margin-bottom: 10px; | |
104 } | |
105 #songList .row.playing { | |
106 box-shadow: 0 0 30px red; | |
107 background-color: #de5050; | |
108 } | 71 } |
109 `, | 72 `, |
110 ]; | 73 ]; |
74 | |
75 constructor() { | |
76 super(); | |
77 getTopGraph().then((g) => { | |
78 this.graph = g; | |
79 this.graph.runHandler(this.updatePlayState.bind(this), "playstate-ui"); | |
80 }); | |
81 setInterval(this.updateT.bind(this), 100); | |
82 } | |
83 | |
84 protected async firstUpdated(_changedProperties: PropertyValues<this>) { | |
85 this.bindKeys(); | |
86 const config = await (await fetch("/service/ascoltami/config")).json(); | |
87 document.title = document.title.replace("{{host}}", config.host); | |
88 this.host = config.host; | |
89 } | |
90 | |
91 updatePlayState() { | |
92 const U = this.graph.U(); | |
93 const asco = U(":ascoltami"); | |
94 this.playerState = { | |
95 duration: this.graph.optionalFloatValue(asco, U(":duration")), | |
96 endOfSong: this.graph.optionalBooleanValue(asco, U(":endOfSong")), | |
97 pausedSongTime: this.graph.optionalFloatValue(asco, U(":pausedSongTime")), | |
98 wallStartTime: this.graph.optionalFloatValue(asco, U(":wallStartTime")), | |
99 playing: this.graph.optionalBooleanValue(asco, U(":playing")), | |
100 song: this.graph.optionalUriValue(asco, U(":song")), | |
101 }; | |
102 this.updateT(); | |
103 } | |
104 | |
105 updateT() { | |
106 if (this.playerState.wallStartTime !== null) { | |
107 this.playerTime = Date.now() / 1000 - this.playerState.wallStartTime; | |
108 } else if (this.playerState.pausedSongTime !== null) { | |
109 this.playerTime = this.playerState.pausedSongTime; | |
110 } else { | |
111 this.playerTime = 0; | |
112 } | |
113 } | |
114 | |
111 render() { | 115 render() { |
112 return html`<rdfdb-synced-graph></rdfdb-synced-graph> | 116 return html` |
113 | 117 <h1>ascoltami on ${this.host}</h1> |
114 <link rel="stylesheet" href="../style.css" /> | 118 |
115 | 119 <span><rdfdb-synced-graph></rdfdb-synced-graph></span> |
116 <!-- <h1>ascoltami <a href="metrics">[metrics]</a></h1> --> | 120 <div id="mainRow"> |
117 | 121 <light9-song-listing |
118 <div id="grow"> | 122 @selectsong=${(ev: any) => { |
119 <span> | 123 this.selectedSong = ev.detail.song; |
120 <div id="songList"> | 124 }} |
121 <table> | 125 .selectedSong=${this.selectedSong} |
122 ${this.songList.map( | 126 ></light9-song-listing> |
123 (song) => html` | 127 <div> |
124 <tr | 128 <light9-ascoltami-timeline .playerState=${this.playerState} .playerTime=${this.playerTime}></light9-ascoltami-timeline> |
125 class="row ${classMap({ | 129 <div> |
126 playing: !!(this.song && song.equals(this.song)), | 130 <button ?disabled=${this.selectedSong == null} @click=${this.onLoadSelected}>Change to and play selected <span class="keyCap">l</span></button> |
127 })}" | 131 <button ?disabled=${false} @click=${this.onCmdZero}>Seek to zero <span class="keyCap">z</span></button> |
128 > | 132 <button ?disabled=${this.playerState.playing || this.playerState.song == null} @click=${this.onCmdPlay}>Play <span class="keyCap">p</span></button> |
129 <td><resource-display .uri=${song} noclick></resource-display></td> | 133 <button ?disabled=${!this.playerState.playing} @click=${this.onCmdStop}>Stop <span class="keyCap">s</span></button> |
130 <td> | 134 <button ?disabled=${true} @click=${this.onCmdGo}>Go <span class="keyCap">g</span></button> |
131 <button @click=${this.onSelectSong.bind(this, song)}> | |
132 <span>Select</span> | |
133 </button> | |
134 </td> | |
135 </tr> | |
136 ` | |
137 )} | |
138 </table> | |
139 </div> </span | |
140 ><span> | |
141 <div id="right"> | |
142 <div> | |
143 Selected: | |
144 <resource-display .uri=${this.selectedSong}></resource-display> | |
145 </div> | |
146 <div> | |
147 <button id="playSelected" ?disabled=${this.selectedSong === null} @click=${this.onPlaySelected}>Play selected from start</button> | |
148 </div> | |
149 </div> | 135 </div> |
150 </span> | 136 ${this.renderPlayerStateTable()} |
137 </div> | |
151 </div> | 138 </div> |
152 | 139 `; |
153 <div class="timeRow"> | 140 } |
154 <div id="timeSlider"></div> | 141 renderPlayerStateTable() { |
155 <light9-timeline-audio id="overview" .show=${this.show} .song=${this.song} .zoom=${this.overviewZoom}> </light9-timeline-audio> | 142 return html` <table> |
156 <light9-timeline-audio id="zoomed" .show=${this.show} .song=${this.song} .zoom=${this.zoom}></light9-timeline-audio> | 143 <tr> |
157 <light9-cursor-canvas id="cursor" .viewState=${this.viewState}></light9-cursor-canvas> | 144 <th>duration</th> |
158 </div> | 145 <td>${this.playerState.duration}</td> |
159 | 146 </tr> |
160 <div class="commands"> | 147 <tr> |
161 <button id="cmd-stop" @click=${this.onCmdStop} class="playMode ${classMap({ active: !this.isPlaying })}"> | 148 <th>endOfSong</th> |
162 <strong>Stop</strong> | 149 <td>${this.playerState.endOfSong}</td> |
163 <div class="key">s</div> | 150 </tr> |
164 </button> | 151 <tr> |
165 <button id="cmd-play" @click=${this.onCmdPlay} class="playMode ${classMap({ active: this.isPlaying })}"> | 152 <th>pausedSongTime</th> |
166 <strong>Play</strong> | 153 <td>${this.playerState.pausedSongTime?.toFixed(3)}</td> |
167 <div class="key">p</div> | 154 </tr> |
168 </button> | 155 <tr> |
169 <button id="cmd-intro" @click=${this.onCmdIntro}> | 156 <th>playing</th> |
170 <strong>Skip intro</strong> | 157 <td>${this.playerState.playing}</td> |
171 <div class="key">i</div> | 158 </tr> |
172 </button> | 159 <tr> |
173 <button id="cmd-post" @click=${this.onCmdPost}> | 160 <th>song</th> |
174 <strong>Skip to Post</strong> | 161 <td>${this.playerState.song?.value}</td> |
175 <div class="key">t</div> | 162 </tr> |
176 </button> | 163 <tr> |
177 <button id="cmd-go" @click=${this.onCmdGo}> | 164 <th>wallStartTime</th> |
178 <strong>Go</strong> | 165 <td>${this.playerState.wallStartTime}</td> |
179 <div class="key">g</div> | 166 </tr> |
180 <div id="next">${this.nextText}</div> | 167 <tr> |
181 </button> | 168 <th>t</th> |
182 </div>`; | 169 <td>${this.playerTime.toFixed(3)}</td> |
170 </tr> | |
171 </table>`; | |
183 } | 172 } |
184 | 173 |
185 onSelectSong(song: NamedNode, ev: MouseEvent) { | 174 onSelectSong(song: NamedNode, ev: MouseEvent) { |
186 if (this.selectedSong && song.equals(this.selectedSong)) { | 175 if (this.selectedSong && song.equals(this.selectedSong)) { |
187 this.selectedSong = null; | 176 this.selectedSong = null; |
188 } else { | 177 } else { |
189 this.selectedSong = song; | 178 this.selectedSong = song; |
190 } | 179 } |
191 } | 180 } |
192 async onPlaySelected(ev: Event) { | 181 |
182 async onLoadSelected() { | |
193 if (!this.selectedSong) { | 183 if (!this.selectedSong) { |
194 return; | 184 return; |
195 } | 185 } |
196 await fetch("../service/ascoltami/song", { method: "POST", body: this.selectedSong.value }); | 186 await fetch("/service/ascoltami/song", { method: "POST", body: this.selectedSong.value }); |
187 this.selectedSong = null; | |
197 } | 188 } |
198 | 189 |
199 onCmdStop(ev?: MouseEvent): void { | 190 onCmdStop(ev?: MouseEvent): void { |
200 postJson("../service/ascoltami/time", { pause: true }); | 191 postJson("/service/ascoltami/time", { pause: true }); |
201 } | 192 } |
193 | |
202 onCmdPlay(ev?: MouseEvent): void { | 194 onCmdPlay(ev?: MouseEvent): void { |
203 postJson("../service/ascoltami/time", { resume: true }); | 195 postJson("/service/ascoltami/time", { resume: true }); |
204 } | 196 } |
205 onCmdIntro(ev?: MouseEvent): void { | 197 |
206 postJson("../service/ascoltami/time", { t: this.times.intro, resume: true }); | |
207 } | |
208 onCmdPost(ev?: MouseEvent): void { | |
209 postJson("../service/ascoltami/time", { | |
210 t: this.currentDuration - this.times.post, | |
211 resume: true, | |
212 }); | |
213 } | |
214 onCmdGo(ev?: MouseEvent): void { | 198 onCmdGo(ev?: MouseEvent): void { |
215 postJson("../service/ascoltami/go", {}); | 199 postJson("/service/ascoltami/go", {}); |
200 } | |
201 | |
202 onCmdZero(ev?: MouseEvent): void { | |
203 postJson("/service/ascoltami/time", { t: 0 }); | |
216 } | 204 } |
217 | 205 |
218 bindKeys() { | 206 bindKeys() { |
219 document.addEventListener("keypress", (ev) => { | 207 document.addEventListener("keypress", (ev) => { |
220 if (ev.which == 115) { | 208 if (ev.key == "l") { |
209 this.onLoadSelected(); | |
210 } else if (ev.key == "z") { | |
211 this.onCmdZero(); | |
212 } else if (ev.key == "p") { | |
213 this.onCmdPlay(); | |
214 } else if (ev.key == "s") { | |
221 this.onCmdStop(); | 215 this.onCmdStop(); |
222 return false; | 216 } else if (ev.key == "g") { |
223 } | |
224 if (ev.which == 112) { | |
225 this.onCmdPlay(); | |
226 return false; | |
227 } | |
228 if (ev.which == 105) { | |
229 this.onCmdIntro(); | |
230 return false; | |
231 } | |
232 if (ev.which == 116) { | |
233 this.onCmdPost(); | |
234 return false; | |
235 } | |
236 | |
237 if (ev.key == "g") { | |
238 this.onCmdGo(); | 217 this.onCmdGo(); |
239 return false; | 218 } else { |
240 } | 219 return true; |
241 return true; | 220 } |
242 }); | 221 }); |
243 } | 222 } |
244 | |
245 async musicSetup() { | |
246 // shoveled over from the vanillajs version | |
247 const config = await (await fetch("../service/ascoltami/config")).json(); | |
248 this.show = new NamedNode(config.show); | |
249 this.times = config.times; | |
250 document.title = document.title.replace("{{host}}", config.host); | |
251 try { | |
252 const h1 = document.querySelector("h1")!; | |
253 h1.innerText = h1.innerText.replace("{{host}}", config.host); | |
254 } catch (e) {} | |
255 | |
256 (window as any).finishOldStyleSetup(this.times, this.onOldStyleUpdate.bind(this)); | |
257 } | |
258 | |
259 onOldStyleUpdate(data: TimingUpdate) { | |
260 this.nextText = data.next; | |
261 this.isPlaying = data.playing; | |
262 this.currentDuration = data.duration; | |
263 this.song = new NamedNode(data.song); | |
264 this.overviewZoom = { duration: data.duration, t1: 0, t2: data.duration }; | |
265 const t1 = data.t - 2, | |
266 t2 = data.t + 20; | |
267 this.zoom = { duration: data.duration, t1, t2 }; | |
268 const timeRow = this.shadowRoot!.querySelector(".timeRow") as HTMLDivElement; | |
269 const w = timeRow.offsetWidth; | |
270 this.viewState = { | |
271 zoomSpec: { t1: () => t1, t2: () => t2 }, | |
272 cursor: { t: () => data.t }, | |
273 audioY: () => 0, | |
274 audioH: () => 60, | |
275 zoomedTimeY: () => 60, | |
276 zoomedTimeH: () => 40, | |
277 fullZoomX: (sec: number) => (sec / data.duration) * w, | |
278 zoomInX: (sec: number) => ((sec - t1) / (t2 - t1)) * w, | |
279 mouse: { pos: () => $V([0, 0]) }, | |
280 }; | |
281 } | |
282 | |
283 @property() songList: NamedNode[] = []; | |
284 constructor() { | |
285 super(); | |
286 this.bindKeys(); | |
287 this.zoom = this.overviewZoom = { duration: null, t1: 0, t2: 1 }; | |
288 | |
289 getTopGraph().then((g) => { | |
290 this.graph = g; | |
291 this.musicSetup(); // async | |
292 this.graph.runHandler(this.graphChanged.bind(this), "loadsongs"); | |
293 }); | |
294 } | |
295 graphChanged() { | |
296 this.songList = []; | |
297 try { | |
298 const playList = this.graph.uriValue( | |
299 // | |
300 this.graph.Uri(showRoot), | |
301 this.graph.Uri(":playList") | |
302 ); | |
303 log(playList); | |
304 this.songList = this.graph.items(playList) as NamedNode[]; | |
305 } catch (e) { | |
306 log("no playlist yet"); | |
307 } | |
308 log(this.songList.length); | |
309 } | |
310 } | 223 } |