ts-video-player 0.1.1 → 0.1.2

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.
package/dist/ui/index.js CHANGED
@@ -1,14 +1,733 @@
1
+ // src/ui/components.ts
2
+ class BaseComponent {
3
+ player = null;
4
+ element = null;
5
+ unsubscribes = [];
6
+ render(player) {
7
+ this.player = player;
8
+ this.element = this.createElement();
9
+ this.attachEvents();
10
+ this.subscribeToState();
11
+ return this.element;
12
+ }
13
+ subscribeToState() {}
14
+ update(_state) {}
15
+ destroy() {
16
+ this.unsubscribes.forEach((fn) => fn());
17
+ this.unsubscribes = [];
18
+ this.element?.remove();
19
+ this.element = null;
20
+ this.player = null;
21
+ }
22
+ el(tag, attrs = {}, children) {
23
+ const element = document.createElement(tag);
24
+ for (const [key, value] of Object.entries(attrs)) {
25
+ if (key === "class") {
26
+ element.className = value;
27
+ } else {
28
+ element.setAttribute(key, value);
29
+ }
30
+ }
31
+ if (children) {
32
+ for (const child of children) {
33
+ if (typeof child === "string") {
34
+ element.appendChild(document.createTextNode(child));
35
+ } else {
36
+ element.appendChild(child);
37
+ }
38
+ }
39
+ }
40
+ return element;
41
+ }
42
+ }
43
+
44
+ class PlayButton extends BaseComponent {
45
+ name = "play-button";
46
+ createElement() {
47
+ return this.el("button", {
48
+ class: "ts-video-btn ts-video-btn--play",
49
+ "aria-label": "Play",
50
+ type: "button"
51
+ }, [this.getIcon()]);
52
+ }
53
+ attachEvents() {
54
+ this.element?.addEventListener("click", () => {
55
+ this.player?.togglePlay();
56
+ });
57
+ }
58
+ subscribeToState() {
59
+ if (!this.player)
60
+ return;
61
+ const unsub = this.player._store?.subscribe("paused", (state) => {
62
+ this.update(state);
63
+ });
64
+ if (unsub)
65
+ this.unsubscribes.push(unsub);
66
+ }
67
+ update(state) {
68
+ if (!this.element)
69
+ return;
70
+ this.element.setAttribute("aria-label", state.paused ? "Play" : "Pause");
71
+ this.element.innerHTML = "";
72
+ this.element.appendChild(this.getIcon(state.paused));
73
+ }
74
+ getIcon(paused = true) {
75
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
76
+ svg.setAttribute("viewBox", "0 0 24 24");
77
+ svg.setAttribute("width", "24");
78
+ svg.setAttribute("height", "24");
79
+ svg.setAttribute("fill", "currentColor");
80
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
81
+ path.setAttribute("d", paused ? "M8 5v14l11-7z" : "M6 19h4V5H6v14zm8-14v14h4V5h-4z");
82
+ svg.appendChild(path);
83
+ return svg;
84
+ }
85
+ }
86
+
87
+ class MuteButton extends BaseComponent {
88
+ name = "mute-button";
89
+ createElement() {
90
+ return this.el("button", {
91
+ class: "ts-video-btn ts-video-btn--mute",
92
+ "aria-label": "Mute",
93
+ type: "button"
94
+ }, [this.getIcon(false, 1)]);
95
+ }
96
+ attachEvents() {
97
+ this.element?.addEventListener("click", () => {
98
+ this.player?.toggleMute();
99
+ });
100
+ }
101
+ update(state) {
102
+ if (!this.element)
103
+ return;
104
+ this.element.setAttribute("aria-label", state.muted ? "Unmute" : "Mute");
105
+ this.element.innerHTML = "";
106
+ this.element.appendChild(this.getIcon(state.muted, state.volume));
107
+ }
108
+ getIcon(muted, volume) {
109
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
110
+ svg.setAttribute("viewBox", "0 0 24 24");
111
+ svg.setAttribute("width", "24");
112
+ svg.setAttribute("height", "24");
113
+ svg.setAttribute("fill", "currentColor");
114
+ let d;
115
+ if (muted || volume === 0) {
116
+ d = "M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z";
117
+ } else if (volume < 0.5) {
118
+ d = "M18.5 12A4.5 4.5 0 0 0 16 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z";
119
+ } else {
120
+ d = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
121
+ }
122
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
123
+ path.setAttribute("d", d);
124
+ svg.appendChild(path);
125
+ return svg;
126
+ }
127
+ }
128
+
129
+ class FullscreenButton extends BaseComponent {
130
+ name = "fullscreen-button";
131
+ createElement() {
132
+ return this.el("button", {
133
+ class: "ts-video-btn ts-video-btn--fullscreen",
134
+ "aria-label": "Enter fullscreen",
135
+ type: "button"
136
+ }, [this.getIcon(false)]);
137
+ }
138
+ attachEvents() {
139
+ this.element?.addEventListener("click", () => {
140
+ this.player?.toggleFullscreen();
141
+ });
142
+ }
143
+ update(state) {
144
+ if (!this.element)
145
+ return;
146
+ const btn = this.element;
147
+ const disabled = state.fullscreenAvailability !== "available";
148
+ btn.disabled = disabled;
149
+ btn.style.display = state.fullscreenAvailability === "unsupported" ? "none" : "";
150
+ btn.setAttribute("aria-label", state.fullscreen ? "Exit fullscreen" : "Enter fullscreen");
151
+ btn.innerHTML = "";
152
+ btn.appendChild(this.getIcon(state.fullscreen));
153
+ }
154
+ getIcon(fullscreen) {
155
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
156
+ svg.setAttribute("viewBox", "0 0 24 24");
157
+ svg.setAttribute("width", "24");
158
+ svg.setAttribute("height", "24");
159
+ svg.setAttribute("fill", "currentColor");
160
+ const d = fullscreen ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z";
161
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
162
+ path.setAttribute("d", d);
163
+ svg.appendChild(path);
164
+ return svg;
165
+ }
166
+ }
167
+
168
+ class PiPButton extends BaseComponent {
169
+ name = "pip-button";
170
+ createElement() {
171
+ return this.el("button", {
172
+ class: "ts-video-btn ts-video-btn--pip",
173
+ "aria-label": "Picture in picture",
174
+ type: "button"
175
+ }, [this.getIcon()]);
176
+ }
177
+ attachEvents() {
178
+ this.element?.addEventListener("click", () => {
179
+ this.player?.togglePiP();
180
+ });
181
+ }
182
+ update(state) {
183
+ if (!this.element)
184
+ return;
185
+ const btn = this.element;
186
+ const disabled = state.pipAvailability !== "available";
187
+ btn.disabled = disabled;
188
+ btn.style.display = state.pipAvailability === "unsupported" ? "none" : "";
189
+ }
190
+ getIcon() {
191
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
192
+ svg.setAttribute("viewBox", "0 0 24 24");
193
+ svg.setAttribute("width", "24");
194
+ svg.setAttribute("height", "24");
195
+ svg.setAttribute("fill", "currentColor");
196
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
197
+ path.setAttribute("d", "M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z");
198
+ svg.appendChild(path);
199
+ return svg;
200
+ }
201
+ }
202
+
203
+ class TimeDisplay extends BaseComponent {
204
+ name = "time-display";
205
+ showRemaining = false;
206
+ createElement() {
207
+ const container = this.el("div", { class: "ts-video-time" });
208
+ const current = this.el("span", { class: "ts-video-time__current" }, ["0:00"]);
209
+ const separator = this.el("span", { class: "ts-video-time__separator" }, [" / "]);
210
+ const duration = this.el("span", { class: "ts-video-time__duration" }, ["0:00"]);
211
+ container.appendChild(current);
212
+ container.appendChild(separator);
213
+ container.appendChild(duration);
214
+ return container;
215
+ }
216
+ attachEvents() {
217
+ this.element?.addEventListener("click", () => {
218
+ this.showRemaining = !this.showRemaining;
219
+ if (this.player) {
220
+ this.update(this.player.state);
221
+ }
222
+ });
223
+ }
224
+ update(state) {
225
+ if (!this.element)
226
+ return;
227
+ const current = this.element.querySelector(".ts-video-time__current");
228
+ const duration = this.element.querySelector(".ts-video-time__duration");
229
+ if (current) {
230
+ current.textContent = formatTime(state.currentTime);
231
+ }
232
+ if (duration) {
233
+ if (this.showRemaining) {
234
+ duration.textContent = `-${formatTime(state.duration - state.currentTime)}`;
235
+ } else {
236
+ duration.textContent = formatTime(state.duration);
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ class Slider extends BaseComponent {
243
+ name = "slider";
244
+ options;
245
+ track = null;
246
+ fill = null;
247
+ thumb = null;
248
+ isDragging = false;
249
+ boundPointerMove;
250
+ boundPointerUp;
251
+ constructor(options) {
252
+ super();
253
+ this.options = {
254
+ orientation: "horizontal",
255
+ disabled: false,
256
+ ...options
257
+ };
258
+ this.boundPointerMove = this.onPointerMove.bind(this);
259
+ this.boundPointerUp = this.onPointerUp.bind(this);
260
+ }
261
+ createElement() {
262
+ const container = this.el("div", {
263
+ class: `ts-video-slider ts-video-slider--${this.options.orientation}`,
264
+ role: "slider",
265
+ "aria-valuemin": String(this.options.min),
266
+ "aria-valuemax": String(this.options.max),
267
+ "aria-valuenow": String(this.options.value),
268
+ tabindex: "0"
269
+ });
270
+ this.track = this.el("div", { class: "ts-video-slider__track" });
271
+ this.fill = this.el("div", { class: "ts-video-slider__fill" });
272
+ this.thumb = this.el("div", { class: "ts-video-slider__thumb" });
273
+ this.track.appendChild(this.fill);
274
+ this.track.appendChild(this.thumb);
275
+ container.appendChild(this.track);
276
+ this.updateVisual(this.options.value);
277
+ return container;
278
+ }
279
+ attachEvents() {
280
+ if (!this.element)
281
+ return;
282
+ this.element.addEventListener("pointerdown", this.onPointerDown.bind(this));
283
+ document.addEventListener("pointermove", this.boundPointerMove);
284
+ document.addEventListener("pointerup", this.boundPointerUp);
285
+ this.element.addEventListener("keydown", this.onKeyDown.bind(this));
286
+ }
287
+ onPointerDown(e) {
288
+ if (this.options.disabled)
289
+ return;
290
+ this.isDragging = true;
291
+ this.element?.setPointerCapture(e.pointerId);
292
+ this.updateFromPointer(e);
293
+ }
294
+ onPointerMove(e) {
295
+ if (!this.isDragging)
296
+ return;
297
+ this.updateFromPointer(e);
298
+ }
299
+ onPointerUp(e) {
300
+ if (!this.isDragging)
301
+ return;
302
+ this.isDragging = false;
303
+ this.element?.releasePointerCapture(e.pointerId);
304
+ this.options.onChange?.(this.options.value);
305
+ }
306
+ onKeyDown(e) {
307
+ if (this.options.disabled)
308
+ return;
309
+ let newValue = this.options.value;
310
+ const step = this.options.step;
311
+ switch (e.key) {
312
+ case "ArrowRight":
313
+ case "ArrowUp":
314
+ newValue = Math.min(this.options.max, newValue + step);
315
+ break;
316
+ case "ArrowLeft":
317
+ case "ArrowDown":
318
+ newValue = Math.max(this.options.min, newValue - step);
319
+ break;
320
+ case "Home":
321
+ newValue = this.options.min;
322
+ break;
323
+ case "End":
324
+ newValue = this.options.max;
325
+ break;
326
+ default:
327
+ return;
328
+ }
329
+ e.preventDefault();
330
+ this.setValue(newValue);
331
+ this.options.onChange?.(newValue);
332
+ }
333
+ updateFromPointer(e) {
334
+ if (!this.track)
335
+ return;
336
+ const rect = this.track.getBoundingClientRect();
337
+ const isHorizontal = this.options.orientation === "horizontal";
338
+ let percent;
339
+ if (isHorizontal) {
340
+ percent = (e.clientX - rect.left) / rect.width;
341
+ } else {
342
+ percent = 1 - (e.clientY - rect.top) / rect.height;
343
+ }
344
+ percent = Math.max(0, Math.min(1, percent));
345
+ const value = this.options.min + percent * (this.options.max - this.options.min);
346
+ const snapped = Math.round(value / this.options.step) * this.options.step;
347
+ this.setValue(snapped);
348
+ this.options.onInput?.(snapped);
349
+ }
350
+ setValue(value) {
351
+ this.options.value = Math.max(this.options.min, Math.min(this.options.max, value));
352
+ this.updateVisual(this.options.value);
353
+ this.element?.setAttribute("aria-valuenow", String(this.options.value));
354
+ }
355
+ updateVisual(value) {
356
+ const percent = (value - this.options.min) / (this.options.max - this.options.min) * 100;
357
+ if (this.fill) {
358
+ if (this.options.orientation === "horizontal") {
359
+ this.fill.style.width = `${percent}%`;
360
+ } else {
361
+ this.fill.style.height = `${percent}%`;
362
+ }
363
+ }
364
+ if (this.thumb) {
365
+ if (this.options.orientation === "horizontal") {
366
+ this.thumb.style.left = `${percent}%`;
367
+ } else {
368
+ this.thumb.style.bottom = `${percent}%`;
369
+ }
370
+ }
371
+ }
372
+ destroy() {
373
+ document.removeEventListener("pointermove", this.boundPointerMove);
374
+ document.removeEventListener("pointerup", this.boundPointerUp);
375
+ super.destroy();
376
+ }
377
+ }
378
+
379
+ class ProgressBar extends BaseComponent {
380
+ name = "progress-bar";
381
+ slider = null;
382
+ buffered = null;
383
+ isSeeking = false;
384
+ createElement() {
385
+ const container = this.el("div", { class: "ts-video-progress" });
386
+ this.buffered = this.el("div", { class: "ts-video-progress__buffered" });
387
+ this.slider = new Slider({
388
+ min: 0,
389
+ max: 100,
390
+ step: 0.1,
391
+ value: 0,
392
+ onInput: (value) => {
393
+ this.isSeeking = true;
394
+ const time = value / 100 * (this.player?.state.duration || 0);
395
+ this.player?.seekTo(time);
396
+ },
397
+ onChange: () => {
398
+ this.isSeeking = false;
399
+ }
400
+ });
401
+ const sliderEl = this.slider.render(this.player);
402
+ sliderEl.classList.add("ts-video-progress__slider");
403
+ const track = sliderEl.querySelector(".ts-video-slider__track");
404
+ if (track) {
405
+ track.insertBefore(this.buffered, track.firstChild);
406
+ }
407
+ container.appendChild(sliderEl);
408
+ return container;
409
+ }
410
+ attachEvents() {}
411
+ update(state) {
412
+ if (!this.slider || this.isSeeking)
413
+ return;
414
+ const percent = state.duration > 0 ? state.currentTime / state.duration * 100 : 0;
415
+ this.slider.setValue(percent);
416
+ if (this.buffered) {
417
+ const bufferedPercent = state.bufferedAmount * 100;
418
+ this.buffered.style.width = `${bufferedPercent}%`;
419
+ }
420
+ }
421
+ destroy() {
422
+ this.slider?.destroy();
423
+ super.destroy();
424
+ }
425
+ }
426
+
427
+ class VolumeSlider extends BaseComponent {
428
+ name = "volume-slider";
429
+ slider = null;
430
+ createElement() {
431
+ const container = this.el("div", { class: "ts-video-volume" });
432
+ this.slider = new Slider({
433
+ min: 0,
434
+ max: 1,
435
+ step: 0.01,
436
+ value: this.player?.state.volume || 1,
437
+ onInput: (value) => {
438
+ this.player?.setVolume(value);
439
+ }
440
+ });
441
+ container.appendChild(this.slider.render(this.player));
442
+ return container;
443
+ }
444
+ attachEvents() {}
445
+ update(state) {
446
+ if (!this.slider)
447
+ return;
448
+ if (this.element) {
449
+ this.element.style.display = state.volumeAvailability === "unsupported" ? "none" : "";
450
+ }
451
+ this.slider.setValue(state.muted ? 0 : state.volume);
452
+ }
453
+ destroy() {
454
+ this.slider?.destroy();
455
+ super.destroy();
456
+ }
457
+ }
458
+
459
+ class Menu extends BaseComponent {
460
+ name = "menu";
461
+ items;
462
+ isOpen = false;
463
+ menuEl = null;
464
+ onSelect;
465
+ constructor(items, onSelect) {
466
+ super();
467
+ this.items = items;
468
+ this.onSelect = onSelect;
469
+ }
470
+ createElement() {
471
+ const container = this.el("div", { class: "ts-video-menu" });
472
+ const trigger = this.el("button", {
473
+ class: "ts-video-btn ts-video-menu__trigger",
474
+ "aria-label": "Settings",
475
+ "aria-haspopup": "true",
476
+ "aria-expanded": "false",
477
+ type: "button"
478
+ }, [this.getIcon()]);
479
+ this.menuEl = this.el("div", {
480
+ class: "ts-video-menu__content",
481
+ role: "menu",
482
+ "aria-hidden": "true"
483
+ });
484
+ this.renderItems();
485
+ container.appendChild(trigger);
486
+ container.appendChild(this.menuEl);
487
+ return container;
488
+ }
489
+ renderItems() {
490
+ if (!this.menuEl)
491
+ return;
492
+ this.menuEl.innerHTML = "";
493
+ for (const item of this.items) {
494
+ const itemEl = this.el("button", {
495
+ class: `ts-video-menu__item${item.checked ? " ts-video-menu__item--checked" : ""}${item.disabled ? " ts-video-menu__item--disabled" : ""}`,
496
+ role: "menuitem",
497
+ "data-id": item.id,
498
+ type: "button"
499
+ }, [item.label]);
500
+ itemEl.addEventListener("click", () => {
501
+ if (!item.disabled) {
502
+ this.onSelect?.(item);
503
+ this.close();
504
+ }
505
+ });
506
+ this.menuEl.appendChild(itemEl);
507
+ }
508
+ }
509
+ attachEvents() {
510
+ const trigger = this.element?.querySelector(".ts-video-menu__trigger");
511
+ trigger?.addEventListener("click", () => this.toggle());
512
+ const onDocClick = (e) => {
513
+ if (this.isOpen && !this.element?.contains(e.target)) {
514
+ this.close();
515
+ }
516
+ };
517
+ document.addEventListener("click", onDocClick);
518
+ this.unsubscribes.push(() => document.removeEventListener("click", onDocClick));
519
+ const onDocKeydown = (e) => {
520
+ if (e.key === "Escape" && this.isOpen) {
521
+ this.close();
522
+ }
523
+ };
524
+ document.addEventListener("keydown", onDocKeydown);
525
+ this.unsubscribes.push(() => document.removeEventListener("keydown", onDocKeydown));
526
+ }
527
+ toggle() {
528
+ if (this.isOpen) {
529
+ this.close();
530
+ } else {
531
+ this.open();
532
+ }
533
+ }
534
+ open() {
535
+ this.isOpen = true;
536
+ this.menuEl?.setAttribute("aria-hidden", "false");
537
+ this.element?.querySelector(".ts-video-menu__trigger")?.setAttribute("aria-expanded", "true");
538
+ this.element?.classList.add("ts-video-menu--open");
539
+ }
540
+ close() {
541
+ this.isOpen = false;
542
+ this.menuEl?.setAttribute("aria-hidden", "true");
543
+ this.element?.querySelector(".ts-video-menu__trigger")?.setAttribute("aria-expanded", "false");
544
+ this.element?.classList.remove("ts-video-menu--open");
545
+ }
546
+ setItems(items) {
547
+ this.items = items;
548
+ this.renderItems();
549
+ }
550
+ getIcon() {
551
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
552
+ svg.setAttribute("viewBox", "0 0 24 24");
553
+ svg.setAttribute("width", "24");
554
+ svg.setAttribute("height", "24");
555
+ svg.setAttribute("fill", "currentColor");
556
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
557
+ path.setAttribute("d", "M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z");
558
+ svg.appendChild(path);
559
+ return svg;
560
+ }
561
+ }
562
+ function formatTime(seconds) {
563
+ if (!isFinite(seconds) || isNaN(seconds))
564
+ return "0:00";
565
+ const negative = seconds < 0;
566
+ const abs = Math.abs(seconds);
567
+ const h = Math.floor(abs / 3600);
568
+ const m = Math.floor(abs % 3600 / 60);
569
+ const s = Math.floor(abs % 60);
570
+ const prefix = negative ? "-" : "";
571
+ if (h > 0) {
572
+ return `${prefix}${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
573
+ }
574
+ return `${prefix}${m}:${s.toString().padStart(2, "0")}`;
575
+ }
576
+ var UI_STYLES = `
577
+ .ts-video-btn {
578
+ display: inline-flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ width: 40px;
582
+ height: 40px;
583
+ padding: 8px;
584
+ background: transparent;
585
+ border: none;
586
+ color: white;
587
+ cursor: pointer;
588
+ transition: opacity 0.2s;
589
+ }
590
+
591
+ .ts-video-btn:hover {
592
+ opacity: 0.8;
593
+ }
594
+
595
+ .ts-video-btn:disabled {
596
+ opacity: 0.4;
597
+ cursor: not-allowed;
598
+ }
599
+
600
+ .ts-video-time {
601
+ font-size: 14px;
602
+ color: white;
603
+ font-variant-numeric: tabular-nums;
604
+ user-select: none;
605
+ cursor: pointer;
606
+ }
607
+
608
+ .ts-video-slider {
609
+ position: relative;
610
+ width: 100%;
611
+ height: 20px;
612
+ cursor: pointer;
613
+ }
614
+
615
+ .ts-video-slider__track {
616
+ position: absolute;
617
+ top: 50%;
618
+ left: 0;
619
+ right: 0;
620
+ height: 4px;
621
+ background: rgba(255, 255, 255, 0.3);
622
+ border-radius: 2px;
623
+ transform: translateY(-50%);
624
+ }
625
+
626
+ .ts-video-slider__fill {
627
+ position: absolute;
628
+ top: 0;
629
+ left: 0;
630
+ height: 100%;
631
+ background: #fff;
632
+ border-radius: 2px;
633
+ }
634
+
635
+ .ts-video-slider__thumb {
636
+ position: absolute;
637
+ top: 50%;
638
+ width: 12px;
639
+ height: 12px;
640
+ background: #fff;
641
+ border-radius: 50%;
642
+ transform: translate(-50%, -50%);
643
+ transition: transform 0.1s;
644
+ }
645
+
646
+ .ts-video-slider:hover .ts-video-slider__thumb {
647
+ transform: translate(-50%, -50%) scale(1.2);
648
+ }
649
+
650
+ .ts-video-progress {
651
+ flex: 1;
652
+ margin: 0 12px;
653
+ }
654
+
655
+ .ts-video-progress__buffered {
656
+ position: absolute;
657
+ top: 0;
658
+ left: 0;
659
+ height: 100%;
660
+ background: rgba(255, 255, 255, 0.4);
661
+ border-radius: 2px;
662
+ }
663
+
664
+ .ts-video-volume {
665
+ width: 80px;
666
+ }
667
+
668
+ .ts-video-menu {
669
+ position: relative;
670
+ }
671
+
672
+ .ts-video-menu__content {
673
+ position: absolute;
674
+ bottom: 100%;
675
+ right: 0;
676
+ min-width: 150px;
677
+ max-height: 200px;
678
+ overflow-y: auto;
679
+ background: rgba(28, 28, 28, 0.95);
680
+ border-radius: 4px;
681
+ padding: 4px 0;
682
+ opacity: 0;
683
+ visibility: hidden;
684
+ transform: translateY(10px);
685
+ transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
686
+ }
687
+
688
+ .ts-video-menu--open .ts-video-menu__content {
689
+ opacity: 1;
690
+ visibility: visible;
691
+ transform: translateY(0);
692
+ }
693
+
694
+ .ts-video-menu__item {
695
+ display: block;
696
+ width: 100%;
697
+ padding: 8px 16px;
698
+ background: none;
699
+ border: none;
700
+ color: white;
701
+ text-align: left;
702
+ cursor: pointer;
703
+ font-size: 14px;
704
+ }
705
+
706
+ .ts-video-menu__item:hover {
707
+ background: rgba(255, 255, 255, 0.1);
708
+ }
709
+
710
+ .ts-video-menu__item--checked::before {
711
+ content: '✓';
712
+ margin-right: 8px;
713
+ }
714
+
715
+ .ts-video-menu__item--disabled {
716
+ opacity: 0.5;
717
+ cursor: not-allowed;
718
+ }
719
+ `;
1
720
  export {
2
- formatTime,
3
- VolumeSlider,
4
- UI_STYLES,
5
- TimeDisplay,
6
- Slider,
7
- ProgressBar,
8
- PlayButton,
9
- PiPButton,
10
- MuteButton,
11
- Menu,
721
+ BaseComponent,
12
722
  FullscreenButton,
13
- BaseComponent
723
+ Menu,
724
+ MuteButton,
725
+ PiPButton,
726
+ PlayButton,
727
+ ProgressBar,
728
+ Slider,
729
+ TimeDisplay,
730
+ UI_STYLES,
731
+ VolumeSlider,
732
+ formatTime
14
733
  };