@unabridged/midwest 0.23.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ const FRICTION = 0.92;
4
+ const MIN_VELOCITY = 0.05;
5
+ const KEY_STEP_FRACTION = 0.12;
6
+ class Panorama extends Controller {
7
+ static targets = ["viewport", "stage", "media", "status", "hint", "reset"];
8
+ static values = {
9
+ wrap: { type: Boolean, default: false },
10
+ autoRotate: { type: Number, default: 0 },
11
+ start: { type: Number, default: 0 }
12
+ };
13
+ offset = 0;
14
+ // current translateX, always <= 0
15
+ mediaWidth = 0;
16
+ // rendered width of a single media copy
17
+ viewportWidth = 0;
18
+ dragging = false;
19
+ pointerId = null;
20
+ lastX = 0;
21
+ lastMoveTime = 0;
22
+ velocity = 0;
23
+ inertiaRaf = null;
24
+ autoRaf = null;
25
+ lastAutoTime = 0;
26
+ clone = null;
27
+ resizeObserver = null;
28
+ interacted = false;
29
+ connect() {
30
+ this.viewportTarget.addEventListener("pointerdown", this.onPointerDown);
31
+ this.element.addEventListener("keydown", this.onKeydown);
32
+ this.element.addEventListener("pointerenter", this.pauseAuto);
33
+ this.element.addEventListener("pointerleave", this.resumeAuto);
34
+ this.element.addEventListener("focusin", this.pauseAuto);
35
+ this.element.addEventListener("focusout", this.resumeAuto);
36
+ this.resizeObserver = new ResizeObserver(() => this.measure());
37
+ this.resizeObserver.observe(this.viewportTarget);
38
+ this.whenMediaReady(() => {
39
+ this.setupWrap();
40
+ this.measure();
41
+ this.offset = this.startOffset();
42
+ this.apply();
43
+ this.startAuto();
44
+ });
45
+ }
46
+ disconnect() {
47
+ this.viewportTarget.removeEventListener("pointerdown", this.onPointerDown);
48
+ window.removeEventListener("pointermove", this.onPointerMove);
49
+ window.removeEventListener("pointerup", this.onPointerUp);
50
+ window.removeEventListener("pointercancel", this.onPointerUp);
51
+ this.element.removeEventListener("keydown", this.onKeydown);
52
+ this.element.removeEventListener("pointerenter", this.pauseAuto);
53
+ this.element.removeEventListener("pointerleave", this.resumeAuto);
54
+ this.element.removeEventListener("focusin", this.pauseAuto);
55
+ this.element.removeEventListener("focusout", this.resumeAuto);
56
+ this.resizeObserver?.disconnect();
57
+ this.stopInertia();
58
+ this.stopAuto();
59
+ }
60
+ // Recenters the view to its starting position (wired to the reset button).
61
+ reset() {
62
+ this.stopInertia();
63
+ this.markInteracted();
64
+ this.offset = this.startOffset();
65
+ this.apply();
66
+ this.announce("View reset");
67
+ }
68
+ // --- media readiness & measurement -----------------------------------------
69
+ whenMediaReady(cb) {
70
+ const media = this.mediaTarget;
71
+ if (media instanceof HTMLImageElement) {
72
+ if (media.complete && media.naturalWidth > 0) {
73
+ cb();
74
+ return;
75
+ }
76
+ media.addEventListener("load", cb, { once: true });
77
+ media.addEventListener("error", cb, { once: true });
78
+ } else {
79
+ if (media.readyState >= 1 && media.videoWidth > 0) {
80
+ cb();
81
+ return;
82
+ }
83
+ media.addEventListener("loadedmetadata", cb, { once: true });
84
+ }
85
+ }
86
+ measure() {
87
+ this.viewportWidth = this.viewportTarget.clientWidth;
88
+ const first = this.stageTarget.querySelector(".midwest-panorama-media");
89
+ this.mediaWidth = first?.getBoundingClientRect().width ?? 0;
90
+ this.offset = this.normalize(this.offset);
91
+ this.apply();
92
+ }
93
+ // Duplicates the image once so the stage can loop seamlessly. Only images
94
+ // wrap — a live <video> can't be cheaply cloned/kept in sync.
95
+ setupWrap() {
96
+ if (!this.wrapValue || this.clone)
97
+ return;
98
+ if (!(this.mediaTarget instanceof HTMLImageElement))
99
+ return;
100
+ const clone = this.mediaTarget.cloneNode(true);
101
+ clone.setAttribute("aria-hidden", "true");
102
+ clone.setAttribute("alt", "");
103
+ clone.removeAttribute("data-midwest-panorama-target");
104
+ this.stageTarget.appendChild(clone);
105
+ this.clone = clone;
106
+ }
107
+ // --- geometry --------------------------------------------------------------
108
+ canWrap() {
109
+ return this.wrapValue && this.clone !== null && this.mediaWidth > 0;
110
+ }
111
+ // Constrains an offset: modulo for seamless wrap, hard clamp otherwise.
112
+ normalize(x) {
113
+ const w = this.mediaWidth;
114
+ if (this.canWrap()) {
115
+ if (w <= 0)
116
+ return 0;
117
+ let n = x % w;
118
+ if (n > 0)
119
+ n -= w;
120
+ return n;
121
+ }
122
+ const min = Math.min(0, this.viewportWidth - w);
123
+ return Math.max(min, Math.min(0, x));
124
+ }
125
+ startOffset() {
126
+ if (this.canWrap())
127
+ return this.normalize(-this.startValue * this.mediaWidth);
128
+ const min = Math.min(0, this.viewportWidth - this.mediaWidth);
129
+ return min * this.startValue;
130
+ }
131
+ apply() {
132
+ this.stageTarget.style.transform = `translate3d(${this.offset}px, 0, 0)`;
133
+ }
134
+ // --- pointer dragging ------------------------------------------------------
135
+ onPointerDown = (event) => {
136
+ if (event.button !== 0)
137
+ return;
138
+ this.dragging = true;
139
+ this.pointerId = event.pointerId;
140
+ this.lastX = event.clientX;
141
+ this.lastMoveTime = performance.now();
142
+ this.velocity = 0;
143
+ this.stopInertia();
144
+ this.pauseAuto();
145
+ this.markInteracted();
146
+ this.element.classList.add("is-dragging");
147
+ this.viewportTarget.setPointerCapture(event.pointerId);
148
+ window.addEventListener("pointermove", this.onPointerMove);
149
+ window.addEventListener("pointerup", this.onPointerUp);
150
+ window.addEventListener("pointercancel", this.onPointerUp);
151
+ };
152
+ onPointerMove = (event) => {
153
+ if (!this.dragging)
154
+ return;
155
+ const dx = event.clientX - this.lastX;
156
+ const now = performance.now();
157
+ const dt = now - this.lastMoveTime;
158
+ if (dt > 0)
159
+ this.velocity = dx / dt * 16;
160
+ this.lastX = event.clientX;
161
+ this.lastMoveTime = now;
162
+ this.offset = this.normalize(this.offset + dx);
163
+ this.apply();
164
+ };
165
+ onPointerUp = () => {
166
+ if (!this.dragging)
167
+ return;
168
+ this.dragging = false;
169
+ this.element.classList.remove("is-dragging");
170
+ if (this.pointerId !== null) {
171
+ try {
172
+ this.viewportTarget.releasePointerCapture(this.pointerId);
173
+ } catch {
174
+ }
175
+ }
176
+ this.pointerId = null;
177
+ window.removeEventListener("pointermove", this.onPointerMove);
178
+ window.removeEventListener("pointerup", this.onPointerUp);
179
+ window.removeEventListener("pointercancel", this.onPointerUp);
180
+ this.startInertia();
181
+ };
182
+ startInertia() {
183
+ if (Math.abs(this.velocity) < MIN_VELOCITY) {
184
+ this.resumeAuto();
185
+ return;
186
+ }
187
+ const step = () => {
188
+ this.offset = this.normalize(this.offset + this.velocity);
189
+ this.apply();
190
+ this.velocity *= FRICTION;
191
+ if (!this.canWrap()) {
192
+ const min = Math.min(0, this.viewportWidth - this.mediaWidth);
193
+ if (this.offset <= min || this.offset >= 0)
194
+ this.velocity = 0;
195
+ }
196
+ if (Math.abs(this.velocity) < MIN_VELOCITY) {
197
+ this.inertiaRaf = null;
198
+ this.resumeAuto();
199
+ return;
200
+ }
201
+ this.inertiaRaf = requestAnimationFrame(step);
202
+ };
203
+ this.inertiaRaf = requestAnimationFrame(step);
204
+ }
205
+ stopInertia() {
206
+ if (this.inertiaRaf !== null) {
207
+ cancelAnimationFrame(this.inertiaRaf);
208
+ this.inertiaRaf = null;
209
+ }
210
+ }
211
+ // --- keyboard --------------------------------------------------------------
212
+ onKeydown = (event) => {
213
+ const step = this.viewportWidth * KEY_STEP_FRACTION;
214
+ let handled = true;
215
+ switch (event.key) {
216
+ case "ArrowLeft":
217
+ this.offset = this.normalize(this.offset + step);
218
+ break;
219
+ case "ArrowRight":
220
+ this.offset = this.normalize(this.offset - step);
221
+ break;
222
+ case "Home":
223
+ this.offset = this.startOffset();
224
+ break;
225
+ default:
226
+ handled = false;
227
+ }
228
+ if (!handled)
229
+ return;
230
+ event.preventDefault();
231
+ this.stopInertia();
232
+ this.pauseAuto();
233
+ this.markInteracted();
234
+ this.apply();
235
+ this.announce();
236
+ };
237
+ // --- auto-rotate -----------------------------------------------------------
238
+ startAuto() {
239
+ if (this.autoRotateValue <= 0 || this.autoRaf !== null || this.dragging)
240
+ return;
241
+ if (this.prefersReducedMotion())
242
+ return;
243
+ this.lastAutoTime = performance.now();
244
+ const step = (now) => {
245
+ const dt = (now - this.lastAutoTime) / 1e3;
246
+ this.lastAutoTime = now;
247
+ this.offset = this.normalize(this.offset - this.autoRotateValue * dt);
248
+ this.apply();
249
+ this.autoRaf = requestAnimationFrame(step);
250
+ };
251
+ this.autoRaf = requestAnimationFrame(step);
252
+ }
253
+ stopAuto() {
254
+ if (this.autoRaf !== null) {
255
+ cancelAnimationFrame(this.autoRaf);
256
+ this.autoRaf = null;
257
+ }
258
+ }
259
+ pauseAuto = () => {
260
+ this.stopAuto();
261
+ };
262
+ resumeAuto = () => {
263
+ if (this.dragging)
264
+ return;
265
+ this.startAuto();
266
+ };
267
+ // --- helpers ---------------------------------------------------------------
268
+ markInteracted() {
269
+ if (this.interacted)
270
+ return;
271
+ this.interacted = true;
272
+ this.element.classList.add("has-interacted");
273
+ }
274
+ announce(message) {
275
+ if (!this.hasStatusTarget)
276
+ return;
277
+ this.statusTarget.textContent = message ?? `Viewing ${this.percent()}%`;
278
+ }
279
+ percent() {
280
+ const w = this.mediaWidth;
281
+ if (w <= 0)
282
+ return 0;
283
+ if (this.canWrap()) {
284
+ const n = (-this.offset % w + w) % w;
285
+ return Math.round(n / w * 100);
286
+ }
287
+ const min = Math.min(0, this.viewportWidth - w);
288
+ return min === 0 ? 0 : Math.round(this.offset / min * 100);
289
+ }
290
+ prefersReducedMotion() {
291
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
292
+ }
293
+ }
294
+
295
+ export { Panorama as default };
296
+ //# sourceMappingURL=panorama_component_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panorama_component_controller.js","sources":["../../../../../../../app/components/midwest/panorama_component/panorama_component_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\n// Inertia decay per frame, and the velocity below which the glide stops.\nconst FRICTION = 0.92\nconst MIN_VELOCITY = 0.05\n// Fraction of the viewport width panned per arrow-key press.\nconst KEY_STEP_FRACTION = 0.12\n\n// Drag-to-pan viewer for wide panoramic images and video. The stage (media +\n// optional seamless-wrap clone) is translated horizontally; pointer, keyboard,\n// inertia, and auto-rotate all funnel through `normalize()` so bounds/wrap are\n// enforced in one place.\nexport default class Panorama extends Controller<HTMLElement> {\n static targets = ['viewport', 'stage', 'media', 'status', 'hint', 'reset']\n static values = {\n wrap: { type: Boolean, default: false },\n autoRotate: { type: Number, default: 0 },\n start: { type: Number, default: 0 }\n }\n\n declare readonly viewportTarget: HTMLElement\n declare readonly stageTarget: HTMLElement\n declare readonly mediaTarget: HTMLImageElement | HTMLVideoElement\n declare readonly statusTarget: HTMLElement\n declare readonly hasStatusTarget: boolean\n declare wrapValue: boolean\n declare autoRotateValue: number\n declare startValue: number\n\n private offset = 0 // current translateX, always <= 0\n private mediaWidth = 0 // rendered width of a single media copy\n private viewportWidth = 0\n private dragging = false\n private pointerId: number | null = null\n private lastX = 0\n private lastMoveTime = 0\n private velocity = 0\n private inertiaRaf: number | null = null\n private autoRaf: number | null = null\n private lastAutoTime = 0\n private clone: HTMLElement | null = null\n private resizeObserver: ResizeObserver | null = null\n private interacted = false\n\n connect () {\n this.viewportTarget.addEventListener('pointerdown', this.onPointerDown)\n this.element.addEventListener('keydown', this.onKeydown)\n this.element.addEventListener('pointerenter', this.pauseAuto)\n this.element.addEventListener('pointerleave', this.resumeAuto)\n this.element.addEventListener('focusin', this.pauseAuto)\n this.element.addEventListener('focusout', this.resumeAuto)\n\n this.resizeObserver = new ResizeObserver(() => this.measure())\n this.resizeObserver.observe(this.viewportTarget)\n\n this.whenMediaReady(() => {\n this.setupWrap()\n this.measure()\n this.offset = this.startOffset()\n this.apply()\n this.startAuto()\n })\n }\n\n disconnect () {\n this.viewportTarget.removeEventListener('pointerdown', this.onPointerDown)\n window.removeEventListener('pointermove', this.onPointerMove)\n window.removeEventListener('pointerup', this.onPointerUp)\n window.removeEventListener('pointercancel', this.onPointerUp)\n this.element.removeEventListener('keydown', this.onKeydown)\n this.element.removeEventListener('pointerenter', this.pauseAuto)\n this.element.removeEventListener('pointerleave', this.resumeAuto)\n this.element.removeEventListener('focusin', this.pauseAuto)\n this.element.removeEventListener('focusout', this.resumeAuto)\n this.resizeObserver?.disconnect()\n this.stopInertia()\n this.stopAuto()\n }\n\n // Recenters the view to its starting position (wired to the reset button).\n reset () {\n this.stopInertia()\n this.markInteracted()\n this.offset = this.startOffset()\n this.apply()\n this.announce('View reset')\n }\n\n // --- media readiness & measurement -----------------------------------------\n\n private whenMediaReady (cb: () => void) {\n const media = this.mediaTarget\n if (media instanceof HTMLImageElement) {\n if (media.complete && media.naturalWidth > 0) { cb(); return }\n media.addEventListener('load', cb, { once: true })\n media.addEventListener('error', cb, { once: true })\n } else {\n if (media.readyState >= 1 && media.videoWidth > 0) { cb(); return }\n media.addEventListener('loadedmetadata', cb, { once: true })\n }\n }\n\n private measure () {\n this.viewportWidth = this.viewportTarget.clientWidth\n const first = this.stageTarget.querySelector('.midwest-panorama-media') as HTMLElement | null\n this.mediaWidth = first?.getBoundingClientRect().width ?? 0\n // Re-clamp after any resize so we never rest out of bounds.\n this.offset = this.normalize(this.offset)\n this.apply()\n }\n\n // Duplicates the image once so the stage can loop seamlessly. Only images\n // wrap — a live <video> can't be cheaply cloned/kept in sync.\n private setupWrap () {\n if (!this.wrapValue || this.clone) return\n if (!(this.mediaTarget instanceof HTMLImageElement)) return\n\n const clone = this.mediaTarget.cloneNode(true) as HTMLElement\n clone.setAttribute('aria-hidden', 'true')\n clone.setAttribute('alt', '')\n clone.removeAttribute('data-midwest-panorama-target')\n this.stageTarget.appendChild(clone)\n this.clone = clone\n }\n\n // --- geometry --------------------------------------------------------------\n\n private canWrap (): boolean {\n return this.wrapValue && this.clone !== null && this.mediaWidth > 0\n }\n\n // Constrains an offset: modulo for seamless wrap, hard clamp otherwise.\n private normalize (x: number): number {\n const w = this.mediaWidth\n if (this.canWrap()) {\n if (w <= 0) return 0\n let n = x % w\n if (n > 0) n -= w\n return n\n }\n const min = Math.min(0, this.viewportWidth - w)\n return Math.max(min, Math.min(0, x))\n }\n\n private startOffset (): number {\n if (this.canWrap()) return this.normalize(-this.startValue * this.mediaWidth)\n const min = Math.min(0, this.viewportWidth - this.mediaWidth)\n return min * this.startValue\n }\n\n private apply () {\n this.stageTarget.style.transform = `translate3d(${this.offset}px, 0, 0)`\n }\n\n // --- pointer dragging ------------------------------------------------------\n\n private onPointerDown = (event: PointerEvent) => {\n if (event.button !== 0) return\n this.dragging = true\n this.pointerId = event.pointerId\n this.lastX = event.clientX\n this.lastMoveTime = performance.now()\n this.velocity = 0\n this.stopInertia()\n this.pauseAuto()\n this.markInteracted()\n this.element.classList.add('is-dragging')\n this.viewportTarget.setPointerCapture(event.pointerId)\n window.addEventListener('pointermove', this.onPointerMove)\n window.addEventListener('pointerup', this.onPointerUp)\n window.addEventListener('pointercancel', this.onPointerUp)\n }\n\n private onPointerMove = (event: PointerEvent) => {\n if (!this.dragging) return\n const dx = event.clientX - this.lastX\n const now = performance.now()\n const dt = now - this.lastMoveTime\n // Normalize velocity to px-per-frame (~16ms) for a consistent glide.\n if (dt > 0) this.velocity = (dx / dt) * 16\n this.lastX = event.clientX\n this.lastMoveTime = now\n this.offset = this.normalize(this.offset + dx)\n this.apply()\n }\n\n private onPointerUp = () => {\n if (!this.dragging) return\n this.dragging = false\n this.element.classList.remove('is-dragging')\n if (this.pointerId !== null) {\n try { this.viewportTarget.releasePointerCapture(this.pointerId) } catch { /* already released */ }\n }\n this.pointerId = null\n window.removeEventListener('pointermove', this.onPointerMove)\n window.removeEventListener('pointerup', this.onPointerUp)\n window.removeEventListener('pointercancel', this.onPointerUp)\n this.startInertia()\n }\n\n private startInertia () {\n if (Math.abs(this.velocity) < MIN_VELOCITY) { this.resumeAuto(); return }\n const step = () => {\n this.offset = this.normalize(this.offset + this.velocity)\n this.apply()\n this.velocity *= FRICTION\n // A hard clamp edge kills the glide immediately.\n if (!this.canWrap()) {\n const min = Math.min(0, this.viewportWidth - this.mediaWidth)\n if (this.offset <= min || this.offset >= 0) this.velocity = 0\n }\n if (Math.abs(this.velocity) < MIN_VELOCITY) {\n this.inertiaRaf = null\n this.resumeAuto()\n return\n }\n this.inertiaRaf = requestAnimationFrame(step)\n }\n this.inertiaRaf = requestAnimationFrame(step)\n }\n\n private stopInertia () {\n if (this.inertiaRaf !== null) {\n cancelAnimationFrame(this.inertiaRaf)\n this.inertiaRaf = null\n }\n }\n\n // --- keyboard --------------------------------------------------------------\n\n private onKeydown = (event: KeyboardEvent) => {\n const step = this.viewportWidth * KEY_STEP_FRACTION\n let handled = true\n switch (event.key) {\n case 'ArrowLeft': this.offset = this.normalize(this.offset + step); break\n case 'ArrowRight': this.offset = this.normalize(this.offset - step); break\n case 'Home': this.offset = this.startOffset(); break\n default: handled = false\n }\n if (!handled) return\n event.preventDefault()\n this.stopInertia()\n this.pauseAuto()\n this.markInteracted()\n this.apply()\n this.announce()\n }\n\n // --- auto-rotate -----------------------------------------------------------\n\n private startAuto () {\n if (this.autoRotateValue <= 0 || this.autoRaf !== null || this.dragging) return\n if (this.prefersReducedMotion()) return\n this.lastAutoTime = performance.now()\n const step = (now: number) => {\n const dt = (now - this.lastAutoTime) / 1000\n this.lastAutoTime = now\n this.offset = this.normalize(this.offset - this.autoRotateValue * dt)\n this.apply()\n this.autoRaf = requestAnimationFrame(step)\n }\n this.autoRaf = requestAnimationFrame(step)\n }\n\n private stopAuto () {\n if (this.autoRaf !== null) {\n cancelAnimationFrame(this.autoRaf)\n this.autoRaf = null\n }\n }\n\n private pauseAuto = () => {\n this.stopAuto()\n }\n\n private resumeAuto = () => {\n if (this.dragging) return\n this.startAuto()\n }\n\n // --- helpers ---------------------------------------------------------------\n\n private markInteracted () {\n if (this.interacted) return\n this.interacted = true\n this.element.classList.add('has-interacted')\n }\n\n private announce (message?: string) {\n if (!this.hasStatusTarget) return\n this.statusTarget.textContent = message ?? `Viewing ${this.percent()}%`\n }\n\n private percent (): number {\n const w = this.mediaWidth\n if (w <= 0) return 0\n if (this.canWrap()) {\n const n = ((-this.offset % w) + w) % w\n return Math.round((n / w) * 100)\n }\n const min = Math.min(0, this.viewportWidth - w)\n return min === 0 ? 0 : Math.round((this.offset / min) * 100)\n }\n\n private prefersReducedMotion (): boolean {\n return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ function formatTime(seconds) {
4
+ const mins = Math.floor(seconds / 60);
5
+ const secs = Math.floor(seconds % 60);
6
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
7
+ }
8
+ class Playlist extends Controller {
9
+ static targets = [
10
+ "audio",
11
+ "audioSource",
12
+ "playIcon",
13
+ "pauseIcon",
14
+ "trackTitle",
15
+ "trackArtist",
16
+ "artworkContainer",
17
+ "placeholderIcon",
18
+ "progressBar",
19
+ "currentTime",
20
+ "totalTime",
21
+ "songList",
22
+ "details"
23
+ ];
24
+ static values = {
25
+ playlistName: { type: String, default: "Playlist" },
26
+ autoplay: { type: Boolean, default: false },
27
+ view: { type: String, default: "playlist" },
28
+ playlistVisible: { type: Boolean, default: true },
29
+ artwork: { type: Boolean, default: false }
30
+ };
31
+ currentIndex = 0;
32
+ songElements = [];
33
+ connect() {
34
+ this.songElements = Array.from(this.songListTarget.querySelectorAll(".midwest-song button"));
35
+ this.updatePlaylistVisibility();
36
+ this.audioTarget.addEventListener("timeupdate", this.handleTimeUpdate);
37
+ this.audioTarget.addEventListener("loadedmetadata", this.handleMetadataLoaded);
38
+ this.audioTarget.addEventListener("ended", this.handleEnded);
39
+ this.audioTarget.addEventListener("play", this.handlePlayEvent);
40
+ this.audioTarget.addEventListener("pause", this.handlePauseEvent);
41
+ this.element.addEventListener("keydown", this.handleKeydown);
42
+ if (this.autoplayValue && this.songElements.length > 0) {
43
+ this.loadSong(0);
44
+ }
45
+ }
46
+ disconnect() {
47
+ this.audioTarget.removeEventListener("timeupdate", this.handleTimeUpdate);
48
+ this.audioTarget.removeEventListener("loadedmetadata", this.handleMetadataLoaded);
49
+ this.audioTarget.removeEventListener("ended", this.handleEnded);
50
+ this.audioTarget.removeEventListener("play", this.handlePlayEvent);
51
+ this.audioTarget.removeEventListener("pause", this.handlePauseEvent);
52
+ this.element.removeEventListener("keydown", this.handleKeydown);
53
+ }
54
+ // Play/pause toggle
55
+ togglePlay() {
56
+ if (this.audioTarget.paused) {
57
+ if (this.audioSourceTarget.src) {
58
+ this.audioTarget.play();
59
+ } else if (this.songElements.length > 0) {
60
+ this.loadSong(0);
61
+ }
62
+ } else {
63
+ this.audioTarget.pause();
64
+ }
65
+ }
66
+ // Play the song whose button was clicked (wired from the song template)
67
+ playSong(event) {
68
+ const button = event.currentTarget;
69
+ const index = this.songElements.indexOf(button);
70
+ if (index === -1)
71
+ return;
72
+ if (index === this.currentIndex && this.audioSourceTarget.src) {
73
+ this.togglePlay();
74
+ return;
75
+ }
76
+ this.loadSong(index);
77
+ }
78
+ // Load and play a specific song by index
79
+ loadSong(index) {
80
+ const songButton = this.songElements[index];
81
+ if (!songButton)
82
+ return;
83
+ const songElement = songButton.closest(".midwest-song");
84
+ if (!songElement)
85
+ return;
86
+ const src = songElement.dataset.src;
87
+ const title = songElement.dataset.title || "Unknown";
88
+ const artist = songElement.dataset.artist || "Unknown";
89
+ const picture = songElement.dataset.picture;
90
+ if (!src)
91
+ return;
92
+ this.audioSourceTarget.src = src;
93
+ this.audioTarget.load();
94
+ this.trackTitleTarget.textContent = title;
95
+ this.trackArtistTarget.textContent = artist;
96
+ if (picture) {
97
+ const img = document.createElement("img");
98
+ img.src = picture;
99
+ img.alt = `Album art for ${title}`;
100
+ img.className = "midwest-playlist__artwork-image";
101
+ const existingImg = this.artworkContainerTarget.querySelector(".midwest-playlist__artwork-image");
102
+ if (existingImg) {
103
+ existingImg.replaceWith(img);
104
+ } else {
105
+ this.artworkContainerTarget.appendChild(img);
106
+ }
107
+ this.placeholderIconTarget.classList.add("hidden");
108
+ } else {
109
+ this.placeholderIconTarget.classList.remove("hidden");
110
+ }
111
+ this.songElements.forEach((btn, i) => {
112
+ const song = btn.closest(".midwest-song");
113
+ if (i === index) {
114
+ song.classList.add("is-playing");
115
+ } else {
116
+ song.classList.remove("is-playing");
117
+ }
118
+ });
119
+ this.currentIndex = index;
120
+ this.audioTarget.play().catch(() => {
121
+ });
122
+ }
123
+ // Play next song
124
+ next() {
125
+ const nextIndex = (this.currentIndex + 1) % this.songElements.length;
126
+ this.loadSong(nextIndex);
127
+ }
128
+ // Play previous song
129
+ previous() {
130
+ const prevIndex = this.currentIndex === 0 ? this.songElements.length - 1 : this.currentIndex - 1;
131
+ this.loadSong(prevIndex);
132
+ }
133
+ // Seek to position in track
134
+ seek(event) {
135
+ const rect = this.progressBarTarget.getBoundingClientRect();
136
+ const x = event.clientX - rect.left;
137
+ const percentage = Math.max(0, Math.min(1, x / rect.width));
138
+ if (this.audioTarget.duration) {
139
+ this.audioTarget.currentTime = percentage * this.audioTarget.duration;
140
+ }
141
+ }
142
+ // Toggle playlist visibility
143
+ togglePlaylist() {
144
+ this.playlistVisibleValue = !this.playlistVisibleValue;
145
+ this.updatePlaylistVisibility();
146
+ this.savePlaylistVisibility();
147
+ }
148
+ // Toggle between playlist and art view
149
+ toggleView() {
150
+ this.viewValue = this.viewValue === "playlist" ? "art" : "playlist";
151
+ this.updateView();
152
+ }
153
+ // Private: Handle time update for progress bar
154
+ handleTimeUpdate = () => {
155
+ const { currentTime, duration } = this.audioTarget;
156
+ if (duration) {
157
+ const percentage = currentTime / duration * 100;
158
+ this.progressBarTarget.value = percentage;
159
+ this.currentTimeTarget.textContent = formatTime(currentTime);
160
+ this.totalTimeTarget.textContent = formatTime(duration);
161
+ }
162
+ };
163
+ // Private: Handle metadata loaded
164
+ handleMetadataLoaded = () => {
165
+ const { duration } = this.audioTarget;
166
+ if (duration) {
167
+ this.totalTimeTarget.textContent = formatTime(duration);
168
+ }
169
+ };
170
+ // Private: advance to the next track when the current one finishes
171
+ handleEnded = () => {
172
+ this.next();
173
+ };
174
+ // Private: reflect the <audio> playing state in the UI
175
+ handlePlayEvent = () => {
176
+ this.playIconTarget.hidden = true;
177
+ this.pauseIconTarget.hidden = false;
178
+ this.element.classList.add("is-playing");
179
+ };
180
+ handlePauseEvent = () => {
181
+ this.playIconTarget.hidden = false;
182
+ this.pauseIconTarget.hidden = true;
183
+ this.element.classList.remove("is-playing");
184
+ };
185
+ // Private: Handle keyboard controls
186
+ handleKeydown = (event) => {
187
+ if (event.key === " " || event.key === "k") {
188
+ event.preventDefault();
189
+ this.togglePlay();
190
+ } else if (event.key === "ArrowLeft") {
191
+ event.preventDefault();
192
+ this.previous();
193
+ } else if (event.key === "ArrowRight") {
194
+ event.preventDefault();
195
+ this.next();
196
+ }
197
+ };
198
+ // Private: Update playlist visibility
199
+ updatePlaylistVisibility() {
200
+ if (this.playlistVisibleValue) {
201
+ this.songListTarget.classList.remove("is-hidden");
202
+ } else {
203
+ this.songListTarget.classList.add("is-hidden");
204
+ }
205
+ }
206
+ // Private: Update view mode
207
+ updateView() {
208
+ this.element.setAttribute("view", this.viewValue);
209
+ }
210
+ // Private: Save playlist visibility to localStorage
211
+ savePlaylistVisibility() {
212
+ try {
213
+ localStorage.setItem(`playlist-${this.playlistNameValue}-visibility`, this.playlistVisibleValue.toString());
214
+ } catch {
215
+ }
216
+ }
217
+ }
218
+
219
+ export { Playlist as default };
220
+ //# sourceMappingURL=playlist_component_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playlist_component_controller.js","sources":["../../../../../../../app/components/midwest/playlist_component/playlist_component_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\n// Format seconds as M:SS or MM:SS\nfunction formatTime(seconds: number): string {\n const mins = Math.floor(seconds / 60)\n const secs = Math.floor(seconds % 60)\n return `${mins}:${secs.toString().padStart(2, '0')}`\n}\n\nexport default class Playlist extends Controller<HTMLElement> {\n static targets = [\n 'audio',\n 'audioSource',\n 'playIcon',\n 'pauseIcon',\n 'trackTitle',\n 'trackArtist',\n 'artworkContainer',\n 'placeholderIcon',\n 'progressBar',\n 'currentTime',\n 'totalTime',\n 'songList',\n 'details'\n ]\n\n static values = {\n playlistName: { type: String, default: 'Playlist' },\n autoplay: { type: Boolean, default: false },\n view: { type: String, default: 'playlist' },\n playlistVisible: { type: Boolean, default: true },\n artwork: { type: Boolean, default: false }\n }\n\n declare readonly audioTarget: HTMLAudioElement\n declare readonly audioSourceTarget: HTMLSourceElement\n declare readonly playIconTarget: HTMLElement\n declare readonly pauseIconTarget: HTMLElement\n declare readonly trackTitleTarget: HTMLHeadingElement\n declare readonly trackArtistTarget: HTMLHeadingElement\n declare readonly artworkContainerTarget: HTMLDivElement\n declare readonly placeholderIconTarget: SVGElement\n declare readonly progressBarTarget: HTMLProgressElement\n declare readonly currentTimeTarget: HTMLHeadingElement\n declare readonly totalTimeTarget: HTMLHeadingElement\n declare readonly songListTarget: HTMLDivElement\n declare readonly detailsTarget: HTMLDivElement\n\n declare playlistNameValue: string\n declare autoplayValue: boolean\n declare viewValue: string\n declare playlistVisibleValue: boolean\n declare artworkValue: boolean\n\n private currentIndex = 0\n private songElements: HTMLElement[] = []\n\n connect () {\n // Find all song buttons in the playlist\n this.songElements = Array.from(this.songListTarget.querySelectorAll<HTMLElement>('.midwest-song button'))\n\n // Set initial playlist visibility\n this.updatePlaylistVisibility()\n\n // Set up audio event listeners (stable references so disconnect can remove them)\n this.audioTarget.addEventListener('timeupdate', this.handleTimeUpdate)\n this.audioTarget.addEventListener('loadedmetadata', this.handleMetadataLoaded)\n this.audioTarget.addEventListener('ended', this.handleEnded)\n this.audioTarget.addEventListener('play', this.handlePlayEvent)\n this.audioTarget.addEventListener('pause', this.handlePauseEvent)\n\n // Set up keyboard controls\n this.element.addEventListener('keydown', this.handleKeydown)\n\n // Auto-play if enabled\n if (this.autoplayValue && this.songElements.length > 0) {\n this.loadSong(0)\n }\n }\n\n disconnect () {\n this.audioTarget.removeEventListener('timeupdate', this.handleTimeUpdate)\n this.audioTarget.removeEventListener('loadedmetadata', this.handleMetadataLoaded)\n this.audioTarget.removeEventListener('ended', this.handleEnded)\n this.audioTarget.removeEventListener('play', this.handlePlayEvent)\n this.audioTarget.removeEventListener('pause', this.handlePauseEvent)\n this.element.removeEventListener('keydown', this.handleKeydown)\n }\n\n // Play/pause toggle\n togglePlay () {\n if (this.audioTarget.paused) {\n if (this.audioSourceTarget.src) {\n this.audioTarget.play()\n } else if (this.songElements.length > 0) {\n this.loadSong(0)\n }\n } else {\n this.audioTarget.pause()\n }\n }\n\n // Play the song whose button was clicked (wired from the song template)\n playSong (event: Event) {\n const button = event.currentTarget as HTMLElement\n const index = this.songElements.indexOf(button)\n if (index === -1) return\n\n // Clicking the already-playing track toggles play/pause instead of reloading\n if (index === this.currentIndex && this.audioSourceTarget.src) {\n this.togglePlay()\n return\n }\n\n this.loadSong(index)\n }\n\n // Load and play a specific song by index\n loadSong (index: number) {\n const songButton = this.songElements[index]\n if (!songButton) return\n\n const songElement = songButton.closest('.midwest-song') as HTMLElement\n if (!songElement) return\n\n // Get song data from data attributes\n const src = songElement.dataset.src\n const title = songElement.dataset.title || 'Unknown'\n const artist = songElement.dataset.artist || 'Unknown'\n const picture = songElement.dataset.picture\n\n if (!src) return\n\n // Update audio source\n this.audioSourceTarget.src = src\n this.audioTarget.load()\n\n // Update track info\n this.trackTitleTarget.textContent = title\n this.trackArtistTarget.textContent = artist\n\n // Update artwork\n if (picture) {\n const img = document.createElement('img')\n img.src = picture\n img.alt = `Album art for ${title}`\n img.className = 'midwest-playlist__artwork-image'\n\n // Remove existing image\n const existingImg = this.artworkContainerTarget.querySelector('.midwest-playlist__artwork-image')\n if (existingImg) {\n existingImg.replaceWith(img)\n } else {\n this.artworkContainerTarget.appendChild(img)\n }\n\n this.placeholderIconTarget.classList.add('hidden')\n } else {\n this.placeholderIconTarget.classList.remove('hidden')\n }\n\n // Update playing state on all songs\n this.songElements.forEach((btn, i) => {\n const song = btn.closest('.midwest-song') as HTMLElement\n if (i === index) {\n song.classList.add('is-playing')\n } else {\n song.classList.remove('is-playing')\n }\n })\n\n this.currentIndex = index\n\n // Auto-play after loading\n this.audioTarget.play().catch(() => {\n // Auto-play was prevented, that's ok\n })\n }\n\n // Play next song\n next () {\n const nextIndex = (this.currentIndex + 1) % this.songElements.length\n this.loadSong(nextIndex)\n }\n\n // Play previous song\n previous () {\n const prevIndex = this.currentIndex === 0\n ? this.songElements.length - 1\n : this.currentIndex - 1\n this.loadSong(prevIndex)\n }\n\n // Seek to position in track\n seek (event: Event) {\n const rect = this.progressBarTarget.getBoundingClientRect()\n const x = (event as MouseEvent).clientX - rect.left\n const percentage = Math.max(0, Math.min(1, x / rect.width))\n\n if (this.audioTarget.duration) {\n this.audioTarget.currentTime = percentage * this.audioTarget.duration\n }\n }\n\n // Toggle playlist visibility\n togglePlaylist () {\n this.playlistVisibleValue = !this.playlistVisibleValue\n this.updatePlaylistVisibility()\n this.savePlaylistVisibility()\n }\n\n // Toggle between playlist and art view\n toggleView () {\n this.viewValue = this.viewValue === 'playlist' ? 'art' : 'playlist'\n this.updateView()\n }\n\n // Private: Handle time update for progress bar\n private handleTimeUpdate = () => {\n const { currentTime, duration } = this.audioTarget\n\n if (duration) {\n const percentage = (currentTime / duration) * 100\n this.progressBarTarget.value = percentage\n\n this.currentTimeTarget.textContent = formatTime(currentTime)\n this.totalTimeTarget.textContent = formatTime(duration)\n }\n }\n\n // Private: Handle metadata loaded\n private handleMetadataLoaded = () => {\n const { duration } = this.audioTarget\n if (duration) {\n this.totalTimeTarget.textContent = formatTime(duration)\n }\n }\n\n // Private: advance to the next track when the current one finishes\n private handleEnded = () => {\n this.next()\n }\n\n // Private: reflect the <audio> playing state in the UI\n private handlePlayEvent = () => {\n this.playIconTarget.hidden = true\n this.pauseIconTarget.hidden = false\n this.element.classList.add('is-playing')\n }\n\n private handlePauseEvent = () => {\n this.playIconTarget.hidden = false\n this.pauseIconTarget.hidden = true\n this.element.classList.remove('is-playing')\n }\n\n // Private: Handle keyboard controls\n private handleKeydown = (event: KeyboardEvent) => {\n if (event.key === ' ' || event.key === 'k') {\n event.preventDefault()\n this.togglePlay()\n } else if (event.key === 'ArrowLeft') {\n event.preventDefault()\n this.previous()\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n this.next()\n }\n }\n\n // Private: Update playlist visibility\n private updatePlaylistVisibility () {\n if (this.playlistVisibleValue) {\n this.songListTarget.classList.remove('is-hidden')\n } else {\n this.songListTarget.classList.add('is-hidden')\n }\n }\n\n // Private: Update view mode\n private updateView () {\n this.element.setAttribute('view', this.viewValue)\n }\n\n // Private: Save playlist visibility to localStorage\n private savePlaylistVisibility () {\n try {\n localStorage.setItem(`playlist-${this.playlistNameValue}-visibility`, this.playlistVisibleValue.toString())\n } catch {\n // Ignore localStorage errors\n }\n }\n}\n"],"names":[],"mappings":"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"}