@fmsim/board 2.0.0-beta.38 → 2.0.0-beta.39
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.
|
@@ -32,6 +32,8 @@ export declare class PlaybackControls extends LitElement {
|
|
|
32
32
|
private _startTime;
|
|
33
33
|
private _seekValue;
|
|
34
34
|
private _currentPlayTime;
|
|
35
|
+
private _seeking;
|
|
36
|
+
private _seekPreviewTime;
|
|
35
37
|
private _speeds;
|
|
36
38
|
private _timer?;
|
|
37
39
|
private get _endTime();
|
|
@@ -47,8 +49,8 @@ export declare class PlaybackControls extends LitElement {
|
|
|
47
49
|
private _onHourChange;
|
|
48
50
|
private _shiftTime;
|
|
49
51
|
/**
|
|
50
|
-
* 시작 시간 변경
|
|
51
|
-
*
|
|
52
|
+
* 시작 시간 변경 시: 재생/일시정지 중이면 새 시간에서 재시작.
|
|
53
|
+
* 정지 상태면 시간만 변경.
|
|
52
54
|
*/
|
|
53
55
|
private _shiftStartTime;
|
|
54
56
|
private _onStart;
|
|
@@ -31,6 +31,8 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
31
31
|
to: new Date()
|
|
32
32
|
};
|
|
33
33
|
this._startTime = PlaybackControls_1._floorToHour(new Date(Date.now() - ONE_HOUR));
|
|
34
|
+
this._seeking = false;
|
|
35
|
+
this._seekPreviewTime = null;
|
|
34
36
|
this._speeds = [1, 1.5, 2];
|
|
35
37
|
}
|
|
36
38
|
get _endTime() {
|
|
@@ -55,7 +57,7 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
55
57
|
if (changes.has('timeRange') && this.timeRange) {
|
|
56
58
|
this._startTime = PlaybackControls_1._floorToHour(this.timeRange.from);
|
|
57
59
|
}
|
|
58
|
-
if (changes.has('currentTime') && this.currentTime) {
|
|
60
|
+
if (changes.has('currentTime') && this.currentTime && !this._seeking) {
|
|
59
61
|
this._updateSeekPosition();
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -111,6 +113,7 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
111
113
|
|
|
112
114
|
<div class="timeline">
|
|
113
115
|
<div class="seek-track">
|
|
116
|
+
<div class="seek-rail"></div>
|
|
114
117
|
${this._renderBufferBars()}
|
|
115
118
|
<input
|
|
116
119
|
type="range"
|
|
@@ -129,7 +132,9 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
129
132
|
</div>
|
|
130
133
|
|
|
131
134
|
<span class="current-time">
|
|
132
|
-
${this.
|
|
135
|
+
${this._seeking && this._seekPreviewTime
|
|
136
|
+
? this._formatTime(this._seekPreviewTime)
|
|
137
|
+
: this._currentPlayTime ? this._formatTime(this._currentPlayTime) : '--:--:--'}
|
|
133
138
|
</span>
|
|
134
139
|
|
|
135
140
|
<md-icon-button @click=${this._onStop}>
|
|
@@ -147,7 +152,7 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
147
152
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
148
153
|
}
|
|
149
154
|
_updateSeekPosition() {
|
|
150
|
-
if (!this.currentTime)
|
|
155
|
+
if (!this.currentTime || this._seeking)
|
|
151
156
|
return;
|
|
152
157
|
const current = new Date(this.currentTime).getTime();
|
|
153
158
|
const from = this._startTime.getTime();
|
|
@@ -179,22 +184,27 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
179
184
|
};
|
|
180
185
|
}
|
|
181
186
|
/**
|
|
182
|
-
* 시작 시간 변경
|
|
183
|
-
*
|
|
187
|
+
* 시작 시간 변경 시: 재생/일시정지 중이면 새 시간에서 재시작.
|
|
188
|
+
* 정지 상태면 시간만 변경.
|
|
184
189
|
*/
|
|
185
190
|
_shiftStartTime(newStart) {
|
|
186
|
-
const
|
|
191
|
+
const wasPlaying = this.playbackState === 'playing';
|
|
192
|
+
const wasPaused = this.playbackState === 'paused';
|
|
187
193
|
this._startTime = newStart;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
this._seekValue = 0;
|
|
195
|
+
this._currentPlayTime = new Date(newStart);
|
|
196
|
+
this._stopTimer();
|
|
197
|
+
if (wasPlaying) {
|
|
198
|
+
// play 중: 새 시간에서 자동 재시작
|
|
199
|
+
this._onStart();
|
|
200
|
+
}
|
|
201
|
+
else if (wasPaused) {
|
|
202
|
+
// pause 중: 엔진에 새 시간 전달 + 데이터 fetch, 하지만 자동 재생 안 함
|
|
203
|
+
this.dispatchEvent(new CustomEvent('playback-seek', {
|
|
204
|
+
detail: { toTime: new Date(newStart) },
|
|
205
|
+
bubbles: true,
|
|
206
|
+
composed: true
|
|
207
|
+
}));
|
|
198
208
|
this._updateSeekFromPlayTime();
|
|
199
209
|
}
|
|
200
210
|
}
|
|
@@ -228,18 +238,21 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
228
238
|
this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }));
|
|
229
239
|
}
|
|
230
240
|
_onSeekInput(e) {
|
|
241
|
+
this._seeking = true;
|
|
231
242
|
this._seekValue = Number(e.target.value);
|
|
232
|
-
// 드래그 중
|
|
243
|
+
// 드래그 중 preview 시간 표시 (실제 seek은 하지 않음)
|
|
233
244
|
const ratio = this._seekValue / 1000;
|
|
234
245
|
const from = this._startTime.getTime();
|
|
235
246
|
const to = this._endTime.getTime();
|
|
236
|
-
this.
|
|
247
|
+
this._seekPreviewTime = new Date(from + (to - from) * ratio);
|
|
237
248
|
}
|
|
238
249
|
_onSeekChange(e) {
|
|
250
|
+
this._seeking = false;
|
|
239
251
|
const ratio = Number(e.target.value) / 1000;
|
|
240
252
|
const from = this._startTime.getTime();
|
|
241
253
|
const to = this._endTime.getTime();
|
|
242
254
|
const targetTime = new Date(from + (to - from) * ratio);
|
|
255
|
+
this._seekPreviewTime = null;
|
|
243
256
|
this._currentPlayTime = targetTime;
|
|
244
257
|
this.dispatchEvent(new CustomEvent('playback-seek', {
|
|
245
258
|
detail: { toTime: targetTime },
|
|
@@ -295,7 +308,8 @@ let PlaybackControls = PlaybackControls_1 = class PlaybackControls extends LitEl
|
|
|
295
308
|
return;
|
|
296
309
|
}
|
|
297
310
|
this._currentPlayTime = next;
|
|
298
|
-
this.
|
|
311
|
+
if (!this._seeking)
|
|
312
|
+
this._updateSeekFromPlayTime();
|
|
299
313
|
this.dispatchEvent(new CustomEvent('playback-tick', {
|
|
300
314
|
detail: { currentTime: this._currentPlayTime },
|
|
301
315
|
bubbles: true,
|
|
@@ -461,6 +475,10 @@ PlaybackControls.styles = css `
|
|
|
461
475
|
--md-icon-button-container-height: 40px;
|
|
462
476
|
}
|
|
463
477
|
|
|
478
|
+
.row-seek md-icon {
|
|
479
|
+
font-variation-settings: 'FILL' 1;
|
|
480
|
+
}
|
|
481
|
+
|
|
464
482
|
.timeline {
|
|
465
483
|
flex: 1;
|
|
466
484
|
display: flex;
|
|
@@ -472,29 +490,47 @@ PlaybackControls.styles = css `
|
|
|
472
490
|
.seek-track {
|
|
473
491
|
position: relative;
|
|
474
492
|
width: 100%;
|
|
493
|
+
height: 12px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.seek-rail {
|
|
497
|
+
position: absolute;
|
|
498
|
+
top: 4px;
|
|
499
|
+
left: 0;
|
|
500
|
+
right: 0;
|
|
475
501
|
height: 4px;
|
|
502
|
+
background: rgba(255, 255, 255, 0.15);
|
|
503
|
+
border-radius: 2px;
|
|
476
504
|
}
|
|
477
505
|
|
|
478
506
|
.buffer-bar {
|
|
479
507
|
position: absolute;
|
|
480
|
-
top:
|
|
481
|
-
height:
|
|
482
|
-
background: rgba(255, 255, 255, 0.
|
|
508
|
+
top: 4px;
|
|
509
|
+
height: 4px;
|
|
510
|
+
background: rgba(255, 255, 255, 0.4);
|
|
483
511
|
border-radius: 2px;
|
|
484
512
|
pointer-events: none;
|
|
513
|
+
z-index: 1;
|
|
485
514
|
}
|
|
486
515
|
|
|
487
516
|
.seek-bar {
|
|
488
|
-
position:
|
|
517
|
+
position: absolute;
|
|
518
|
+
top: 0;
|
|
519
|
+
left: 0;
|
|
489
520
|
width: 100%;
|
|
490
|
-
height:
|
|
521
|
+
height: 12px;
|
|
491
522
|
-webkit-appearance: none;
|
|
492
523
|
appearance: none;
|
|
493
|
-
background:
|
|
494
|
-
border-radius: 2px;
|
|
524
|
+
background: transparent;
|
|
495
525
|
outline: none;
|
|
496
526
|
cursor: pointer;
|
|
497
|
-
z-index:
|
|
527
|
+
z-index: 2;
|
|
528
|
+
margin: 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.seek-bar::-webkit-slider-runnable-track {
|
|
532
|
+
height: 12px;
|
|
533
|
+
background: transparent;
|
|
498
534
|
}
|
|
499
535
|
|
|
500
536
|
.seek-bar::-webkit-slider-thumb {
|
|
@@ -545,6 +581,12 @@ __decorate([
|
|
|
545
581
|
__decorate([
|
|
546
582
|
state()
|
|
547
583
|
], PlaybackControls.prototype, "_currentPlayTime", void 0);
|
|
584
|
+
__decorate([
|
|
585
|
+
state()
|
|
586
|
+
], PlaybackControls.prototype, "_seeking", void 0);
|
|
587
|
+
__decorate([
|
|
588
|
+
state()
|
|
589
|
+
], PlaybackControls.prototype, "_seekPreviewTime", void 0);
|
|
548
590
|
PlaybackControls = PlaybackControls_1 = __decorate([
|
|
549
591
|
customElement('ox-playback-controls')
|
|
550
592
|
], PlaybackControls);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ox-playback-controls.js","sourceRoot":"","sources":["../../src/ox-playback-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/iconbutton/icon-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\ntype PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nconst ONE_HOUR = 3600000\n\n/**\n * 플레이백 컨트롤 오버레이 바 (2행 구조)\n *\n * 1행: 시작 시점 설정 (datetime + ±1h 버튼) + 배속 선택\n * 2행: 재생 제어 (play/pause/stop) + 1시간 범위 시크바 + 닫기\n *\n * @fires playback-start - 플레이백 시작 요청 { fromTime, speed }\n * @fires playback-pause - 일시정지 요청\n * @fires playback-resume - 재개 요청\n * @fires playback-stop - 중지 요청\n * @fires playback-seek - 시크 요청 { toTime }\n * @fires playback-speed - 배속 변경 요청 { speed }\n */\n@customElement('ox-playback-controls')\nexport class PlaybackControls extends LitElement {\n static styles = css`\n :host {\n display: flex;\n justify-content: center;\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1001;\n pointer-events: none;\n }\n\n .control-bar {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 16px;\n margin: 12px;\n max-width: 560px;\n width: 100%;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(8px);\n border-radius: 12px;\n color: #fff;\n pointer-events: auto;\n font-size: 13px;\n box-sizing: border-box;\n }\n\n /* 1행: 시점 설정 + 배속 */\n .row-time {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n md-icon-button {\n --md-icon-button-icon-color: #fff;\n --md-icon-button-hover-icon-color: #fff;\n --md-icon-button-pressed-icon-color: #fff;\n --md-icon-button-focus-icon-color: #fff;\n --md-icon-button-state-layer-color: rgba(255, 255, 255, 0.2);\n }\n\n md-icon-button:hover {\n background: rgba(255, 255, 255, 0.15);\n border-radius: 50%;\n }\n\n .row-time md-icon-button {\n --md-icon-button-icon-size: 18px;\n --md-icon-button-container-width: 32px;\n --md-icon-button-container-height: 32px;\n }\n\n .date-input {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 8px;\n outline: none;\n color-scheme: dark;\n width: 130px;\n }\n\n .date-input:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .hour-select {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 6px;\n outline: none;\n color-scheme: dark;\n cursor: pointer;\n }\n\n .hour-select:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .time-label {\n font-size: 12px;\n opacity: 0.7;\n white-space: nowrap;\n }\n\n .spacer {\n flex: 1;\n }\n\n .speed-group {\n display: flex;\n gap: 2px;\n }\n\n .speed-btn {\n padding: 4px 8px;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n background: transparent;\n color: #fff;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n }\n\n .speed-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n .speed-btn.active {\n background: rgba(255, 255, 255, 0.3);\n border-color: #fff;\n }\n\n /* 2행: 재생 제어 + 시크바 */\n .row-seek {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .row-seek md-icon-button {\n --md-icon-button-icon-size: 20px;\n --md-icon-button-container-width: 36px;\n --md-icon-button-container-height: 36px;\n }\n\n .row-seek md-icon-button.primary {\n --md-icon-button-icon-size: 24px;\n --md-icon-button-container-width: 40px;\n --md-icon-button-container-height: 40px;\n }\n\n .timeline {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n }\n\n .seek-track {\n position: relative;\n width: 100%;\n height: 4px;\n }\n\n .buffer-bar {\n position: absolute;\n top: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.25);\n border-radius: 2px;\n pointer-events: none;\n }\n\n .seek-bar {\n position: relative;\n width: 100%;\n height: 4px;\n -webkit-appearance: none;\n appearance: none;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n z-index: 1;\n }\n\n .seek-bar::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n cursor: pointer;\n }\n\n .seek-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n opacity: 0.8;\n }\n\n .current-time {\n font-size: 12px;\n opacity: 0.9;\n white-space: nowrap;\n min-width: 60px;\n text-align: center;\n }\n `\n\n @property({ type: String }) playbackState: PlaybackState = 'idle'\n @property({ type: Number }) speed: number = 1\n @property({ type: String }) currentTime: string = ''\n @property({ type: Array }) bufferedRanges: { from: number; to: number }[] = []\n\n /** 외부에서 전달받은 초기 시간 범위 (from만 사용, to는 from+1h로 자동 계산) */\n @property({ type: Object }) timeRange: { from: Date; to: Date } = {\n from: new Date(Date.now() - ONE_HOUR),\n to: new Date()\n }\n\n @state() private _startTime: Date = PlaybackControls._floorToHour(new Date(Date.now() - ONE_HOUR))\n @state() declare private _seekValue: number\n @state() declare private _currentPlayTime: Date | null\n\n private _speeds = [1, 1.5, 2]\n private _timer?: number\n\n private get _endTime(): Date {\n return new Date(this._startTime.getTime() + ONE_HOUR)\n }\n\n private static _floorToHour(date: Date): Date {\n const d = new Date(date)\n d.setMinutes(0, 0, 0)\n return d\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n this._seekValue = 0\n this._currentPlayTime = null\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this._stopTimer()\n }\n\n updated(changes: Map<string, any>) {\n if (changes.has('timeRange') && this.timeRange) {\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n }\n if (changes.has('currentTime') && this.currentTime) {\n this._updateSeekPosition()\n }\n }\n\n render() {\n const isPlaying = this.playbackState === 'playing'\n const isPaused = this.playbackState === 'paused'\n const isActive = isPlaying || isPaused\n\n return html`\n <div class=\"control-bar\">\n <!-- 1행: 시점 설정 + 배속 -->\n <div class=\"row-time\">\n <md-icon-button @click=${this._shiftTime(-1)} title=\"-1h\">\n <md-icon>chevron_left</md-icon>\n </md-icon-button>\n\n <input\n type=\"date\"\n class=\"date-input\"\n .value=${this._toDateString(this._startTime)}\n @change=${this._onDateChange}\n />\n\n <select class=\"hour-select\" .value=${String(this._startTime.getHours())} @change=${this._onHourChange}>\n ${Array.from({ length: 24 }, (_, i) => html`\n <option value=${i} ?selected=${this._startTime.getHours() === i}>\n ${String(i).padStart(2, '0')}:00\n </option>\n `)}\n </select>\n\n <md-icon-button @click=${this._shiftTime(1)} title=\"+1h\">\n <md-icon>chevron_right</md-icon>\n </md-icon-button>\n\n <span class=\"time-label\">~ ${String(this._endTime.getHours()).padStart(2, '0')}:00</span>\n\n <span class=\"spacer\"></span>\n\n <div class=\"speed-group\">\n ${this._speeds.map(\n s => html`\n <button class=\"speed-btn ${this.speed === s ? 'active' : ''}\" @click=${() => this._onSpeedChange(s)}>\n ×${s}\n </button>\n `\n )}\n </div>\n </div>\n\n <!-- 2행: 재생 제어 + 시크바 + 닫기 -->\n <div class=\"row-seek\">\n <md-icon-button class=\"primary\" @click=${isActive ? this._onTogglePlayPause : this._onStart}>\n <md-icon>${isPlaying ? 'pause' : 'play_arrow'}</md-icon>\n </md-icon-button>\n\n <div class=\"timeline\">\n <div class=\"seek-track\">\n ${this._renderBufferBars()}\n <input\n type=\"range\"\n class=\"seek-bar\"\n min=\"0\"\n max=\"1000\"\n .value=${String(this._seekValue)}\n @input=${this._onSeekInput}\n @change=${this._onSeekChange}\n />\n </div>\n <div class=\"seek-labels\">\n <span>${this._formatTime(this._startTime)}</span>\n <span>${this._formatTime(this._endTime)}</span>\n </div>\n </div>\n\n <span class=\"current-time\">\n ${this._currentPlayTime ? this._formatTime(this._currentPlayTime) : '--:--:--'}\n </span>\n\n <md-icon-button @click=${this._onStop}>\n <md-icon>close</md-icon>\n </md-icon-button>\n </div>\n </div>\n `\n }\n\n private _formatTime(date: Date): string {\n return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })\n }\n\n private _toDateString(date: Date): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n }\n\n private _updateSeekPosition() {\n if (!this.currentTime) return\n const current = new Date(this.currentTime).getTime()\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((current - from) / range) * 1000)))\n }\n\n private _onDateChange(e: Event) {\n const value = (e.target as HTMLInputElement).value\n if (!value) return\n const [year, month, day] = value.split('-').map(Number)\n const newStart = new Date(this._startTime)\n newStart.setFullYear(year, month - 1, day)\n this._shiftStartTime(newStart)\n }\n\n private _onHourChange(e: Event) {\n const hour = Number((e.target as HTMLSelectElement).value)\n const newStart = new Date(this._startTime)\n newStart.setHours(hour, 0, 0, 0)\n this._shiftStartTime(newStart)\n }\n\n private _shiftTime(hours: number) {\n return () => {\n const newStart = new Date(this._startTime.getTime() + hours * ONE_HOUR)\n this._shiftStartTime(newStart)\n }\n }\n\n /**\n * 시작 시간 변경 시 현재 재생 시간도 같은 offset만큼 이동.\n * 재생 중이면 타이머 유지.\n */\n private _shiftStartTime(newStart: Date) {\n const delta = newStart.getTime() - this._startTime.getTime()\n this._startTime = newStart\n\n if (this._currentPlayTime) {\n const shifted = new Date(this._currentPlayTime.getTime() + delta)\n const end = this._endTime\n\n // 새 범위 안에 있으면 이동, 밖이면 시작점으로\n if (shifted.getTime() >= newStart.getTime() && shifted.getTime() <= end.getTime()) {\n this._currentPlayTime = shifted\n } else {\n this._currentPlayTime = new Date(newStart)\n }\n\n this._updateSeekFromPlayTime()\n }\n }\n\n private _onStart() {\n this._currentPlayTime = new Date(this._startTime)\n this.playbackState = 'playing'\n this._startTimer()\n\n this.dispatchEvent(\n new CustomEvent('playback-start', {\n detail: { fromTime: this._startTime, speed: this.speed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onTogglePlayPause() {\n if (this.playbackState === 'playing') {\n this.playbackState = 'paused'\n this._stopTimer()\n this.dispatchEvent(new CustomEvent('playback-pause', { bubbles: true, composed: true }))\n } else {\n this.playbackState = 'playing'\n this._startTimer()\n this.dispatchEvent(new CustomEvent('playback-resume', { bubbles: true, composed: true }))\n }\n }\n\n private _onStop() {\n this._stopTimer()\n this._currentPlayTime = null\n this._seekValue = 0\n this.playbackState = 'idle'\n this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }))\n }\n\n private _onSeekInput(e: Event) {\n this._seekValue = Number((e.target as HTMLInputElement).value)\n\n // 드래그 중 현재 시간 업데이트 (비주얼 피드백)\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n this._currentPlayTime = new Date(from + (to - from) * ratio)\n }\n\n private _onSeekChange(e: Event) {\n const ratio = Number((e.target as HTMLInputElement).value) / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const targetTime = new Date(from + (to - from) * ratio)\n\n this._currentPlayTime = targetTime\n\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: targetTime },\n bubbles: true,\n composed: true\n })\n )\n\n // 재생 중이었으면 새 시점에서 타이머 계속\n if (this.playbackState === 'playing') {\n this._stopTimer()\n this._startTimer()\n }\n }\n\n private _renderBufferBars() {\n if (!this.bufferedRanges?.length) return ''\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return ''\n\n return this.bufferedRanges.map(r => {\n const left = Math.max(0, (r.from - from) / range) * 100\n const right = Math.min(1, (r.to - from) / range) * 100\n const width = right - left\n return html`<div class=\"buffer-bar\" style=\"left:${left}%;width:${width}%\"></div>`\n })\n }\n\n private _onSpeedChange(speed: number) {\n this.speed = speed\n\n this.dispatchEvent(\n new CustomEvent('playback-speed', {\n detail: { speed },\n bubbles: true,\n composed: true\n })\n )\n\n // 재생 중이면 타이머 재시작 (새 속도 반영)\n if (this.playbackState === 'playing') {\n this._stopTimer()\n this._startTimer()\n }\n }\n\n // --- 타이머: 초 단위 진행 ---\n\n private _startTimer() {\n this._stopTimer()\n\n const intervalMs = 1000 / this.speed\n this._timer = window.setInterval(() => {\n if (!this._currentPlayTime) return\n\n const next = new Date(this._currentPlayTime.getTime() + 1000)\n\n // 종료 시점 초과 시 정지\n if (next.getTime() > this._endTime.getTime()) {\n this._onStop()\n return\n }\n\n this._currentPlayTime = next\n this._updateSeekFromPlayTime()\n\n this.dispatchEvent(\n new CustomEvent('playback-tick', {\n detail: { currentTime: this._currentPlayTime },\n bubbles: true,\n composed: true\n })\n )\n }, intervalMs)\n }\n\n private _stopTimer() {\n if (this._timer) {\n clearInterval(this._timer)\n this._timer = undefined\n }\n }\n\n private _updateSeekFromPlayTime() {\n if (!this._currentPlayTime) return\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((this._currentPlayTime.getTime() - from) / range) * 1000)))\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ox-playback-controls.js","sourceRoot":"","sources":["../../src/ox-playback-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/iconbutton/icon-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\ntype PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped'\n\nconst ONE_HOUR = 3600000\n\n/**\n * 플레이백 컨트롤 오버레이 바 (2행 구조)\n *\n * 1행: 시작 시점 설정 (datetime + ±1h 버튼) + 배속 선택\n * 2행: 재생 제어 (play/pause/stop) + 1시간 범위 시크바 + 닫기\n *\n * @fires playback-start - 플레이백 시작 요청 { fromTime, speed }\n * @fires playback-pause - 일시정지 요청\n * @fires playback-resume - 재개 요청\n * @fires playback-stop - 중지 요청\n * @fires playback-seek - 시크 요청 { toTime }\n * @fires playback-speed - 배속 변경 요청 { speed }\n */\n@customElement('ox-playback-controls')\nexport class PlaybackControls extends LitElement {\n static styles = css`\n :host {\n display: flex;\n justify-content: center;\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1001;\n pointer-events: none;\n }\n\n .control-bar {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 16px;\n margin: 12px;\n max-width: 560px;\n width: 100%;\n background: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(8px);\n border-radius: 12px;\n color: #fff;\n pointer-events: auto;\n font-size: 13px;\n box-sizing: border-box;\n }\n\n /* 1행: 시점 설정 + 배속 */\n .row-time {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n md-icon-button {\n --md-icon-button-icon-color: #fff;\n --md-icon-button-hover-icon-color: #fff;\n --md-icon-button-pressed-icon-color: #fff;\n --md-icon-button-focus-icon-color: #fff;\n --md-icon-button-state-layer-color: rgba(255, 255, 255, 0.2);\n }\n\n md-icon-button:hover {\n background: rgba(255, 255, 255, 0.15);\n border-radius: 50%;\n }\n\n .row-time md-icon-button {\n --md-icon-button-icon-size: 18px;\n --md-icon-button-container-width: 32px;\n --md-icon-button-container-height: 32px;\n }\n\n .date-input {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 8px;\n outline: none;\n color-scheme: dark;\n width: 130px;\n }\n\n .date-input:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .hour-select {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 6px;\n color: #fff;\n font-size: 13px;\n padding: 4px 6px;\n outline: none;\n color-scheme: dark;\n cursor: pointer;\n }\n\n .hour-select:focus {\n border-color: rgba(255, 255, 255, 0.6);\n }\n\n .time-label {\n font-size: 12px;\n opacity: 0.7;\n white-space: nowrap;\n }\n\n .spacer {\n flex: 1;\n }\n\n .speed-group {\n display: flex;\n gap: 2px;\n }\n\n .speed-btn {\n padding: 4px 8px;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n background: transparent;\n color: #fff;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n }\n\n .speed-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n .speed-btn.active {\n background: rgba(255, 255, 255, 0.3);\n border-color: #fff;\n }\n\n /* 2행: 재생 제어 + 시크바 */\n .row-seek {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .row-seek md-icon-button {\n --md-icon-button-icon-size: 20px;\n --md-icon-button-container-width: 36px;\n --md-icon-button-container-height: 36px;\n }\n\n .row-seek md-icon-button.primary {\n --md-icon-button-icon-size: 24px;\n --md-icon-button-container-width: 40px;\n --md-icon-button-container-height: 40px;\n }\n\n .row-seek md-icon {\n font-variation-settings: 'FILL' 1;\n }\n\n .timeline {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n }\n\n .seek-track {\n position: relative;\n width: 100%;\n height: 12px;\n }\n\n .seek-rail {\n position: absolute;\n top: 4px;\n left: 0;\n right: 0;\n height: 4px;\n background: rgba(255, 255, 255, 0.15);\n border-radius: 2px;\n }\n\n .buffer-bar {\n position: absolute;\n top: 4px;\n height: 4px;\n background: rgba(255, 255, 255, 0.4);\n border-radius: 2px;\n pointer-events: none;\n z-index: 1;\n }\n\n .seek-bar {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 12px;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n outline: none;\n cursor: pointer;\n z-index: 2;\n margin: 0;\n }\n\n .seek-bar::-webkit-slider-runnable-track {\n height: 12px;\n background: transparent;\n }\n\n .seek-bar::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n cursor: pointer;\n }\n\n .seek-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n opacity: 0.8;\n }\n\n .current-time {\n font-size: 12px;\n opacity: 0.9;\n white-space: nowrap;\n min-width: 60px;\n text-align: center;\n }\n `\n\n @property({ type: String }) playbackState: PlaybackState = 'idle'\n @property({ type: Number }) speed: number = 1\n @property({ type: String }) currentTime: string = ''\n @property({ type: Array }) bufferedRanges: { from: number; to: number }[] = []\n\n /** 외부에서 전달받은 초기 시간 범위 (from만 사용, to는 from+1h로 자동 계산) */\n @property({ type: Object }) timeRange: { from: Date; to: Date } = {\n from: new Date(Date.now() - ONE_HOUR),\n to: new Date()\n }\n\n @state() private _startTime: Date = PlaybackControls._floorToHour(new Date(Date.now() - ONE_HOUR))\n @state() declare private _seekValue: number\n @state() declare private _currentPlayTime: Date | null\n @state() private _seeking = false\n @state() private _seekPreviewTime: Date | null = null\n\n private _speeds = [1, 1.5, 2]\n private _timer?: number\n\n private get _endTime(): Date {\n return new Date(this._startTime.getTime() + ONE_HOUR)\n }\n\n private static _floorToHour(date: Date): Date {\n const d = new Date(date)\n d.setMinutes(0, 0, 0)\n return d\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n this._seekValue = 0\n this._currentPlayTime = null\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this._stopTimer()\n }\n\n updated(changes: Map<string, any>) {\n if (changes.has('timeRange') && this.timeRange) {\n this._startTime = PlaybackControls._floorToHour(this.timeRange.from)\n }\n if (changes.has('currentTime') && this.currentTime && !this._seeking) {\n this._updateSeekPosition()\n }\n }\n\n render() {\n const isPlaying = this.playbackState === 'playing'\n const isPaused = this.playbackState === 'paused'\n const isActive = isPlaying || isPaused\n\n return html`\n <div class=\"control-bar\">\n <!-- 1행: 시점 설정 + 배속 -->\n <div class=\"row-time\">\n <md-icon-button @click=${this._shiftTime(-1)} title=\"-1h\">\n <md-icon>chevron_left</md-icon>\n </md-icon-button>\n\n <input\n type=\"date\"\n class=\"date-input\"\n .value=${this._toDateString(this._startTime)}\n @change=${this._onDateChange}\n />\n\n <select class=\"hour-select\" .value=${String(this._startTime.getHours())} @change=${this._onHourChange}>\n ${Array.from({ length: 24 }, (_, i) => html`\n <option value=${i} ?selected=${this._startTime.getHours() === i}>\n ${String(i).padStart(2, '0')}:00\n </option>\n `)}\n </select>\n\n <md-icon-button @click=${this._shiftTime(1)} title=\"+1h\">\n <md-icon>chevron_right</md-icon>\n </md-icon-button>\n\n <span class=\"time-label\">~ ${String(this._endTime.getHours()).padStart(2, '0')}:00</span>\n\n <span class=\"spacer\"></span>\n\n <div class=\"speed-group\">\n ${this._speeds.map(\n s => html`\n <button class=\"speed-btn ${this.speed === s ? 'active' : ''}\" @click=${() => this._onSpeedChange(s)}>\n ×${s}\n </button>\n `\n )}\n </div>\n </div>\n\n <!-- 2행: 재생 제어 + 시크바 + 닫기 -->\n <div class=\"row-seek\">\n <md-icon-button class=\"primary\" @click=${isActive ? this._onTogglePlayPause : this._onStart}>\n <md-icon>${isPlaying ? 'pause' : 'play_arrow'}</md-icon>\n </md-icon-button>\n\n <div class=\"timeline\">\n <div class=\"seek-track\">\n <div class=\"seek-rail\"></div>\n ${this._renderBufferBars()}\n <input\n type=\"range\"\n class=\"seek-bar\"\n min=\"0\"\n max=\"1000\"\n .value=${String(this._seekValue)}\n @input=${this._onSeekInput}\n @change=${this._onSeekChange}\n />\n </div>\n <div class=\"seek-labels\">\n <span>${this._formatTime(this._startTime)}</span>\n <span>${this._formatTime(this._endTime)}</span>\n </div>\n </div>\n\n <span class=\"current-time\">\n ${this._seeking && this._seekPreviewTime\n ? this._formatTime(this._seekPreviewTime)\n : this._currentPlayTime ? this._formatTime(this._currentPlayTime) : '--:--:--'}\n </span>\n\n <md-icon-button @click=${this._onStop}>\n <md-icon>close</md-icon>\n </md-icon-button>\n </div>\n </div>\n `\n }\n\n private _formatTime(date: Date): string {\n return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })\n }\n\n private _toDateString(date: Date): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n }\n\n private _updateSeekPosition() {\n if (!this.currentTime || this._seeking) return\n const current = new Date(this.currentTime).getTime()\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((current - from) / range) * 1000)))\n }\n\n private _onDateChange(e: Event) {\n const value = (e.target as HTMLInputElement).value\n if (!value) return\n const [year, month, day] = value.split('-').map(Number)\n const newStart = new Date(this._startTime)\n newStart.setFullYear(year, month - 1, day)\n this._shiftStartTime(newStart)\n }\n\n private _onHourChange(e: Event) {\n const hour = Number((e.target as HTMLSelectElement).value)\n const newStart = new Date(this._startTime)\n newStart.setHours(hour, 0, 0, 0)\n this._shiftStartTime(newStart)\n }\n\n private _shiftTime(hours: number) {\n return () => {\n const newStart = new Date(this._startTime.getTime() + hours * ONE_HOUR)\n this._shiftStartTime(newStart)\n }\n }\n\n /**\n * 시작 시간 변경 시: 재생/일시정지 중이면 새 시간에서 재시작.\n * 정지 상태면 시간만 변경.\n */\n private _shiftStartTime(newStart: Date) {\n const wasPlaying = this.playbackState === 'playing'\n const wasPaused = this.playbackState === 'paused'\n\n this._startTime = newStart\n this._seekValue = 0\n this._currentPlayTime = new Date(newStart)\n this._stopTimer()\n\n if (wasPlaying) {\n // play 중: 새 시간에서 자동 재시작\n this._onStart()\n } else if (wasPaused) {\n // pause 중: 엔진에 새 시간 전달 + 데이터 fetch, 하지만 자동 재생 안 함\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: new Date(newStart) },\n bubbles: true,\n composed: true\n })\n )\n this._updateSeekFromPlayTime()\n }\n }\n\n private _onStart() {\n this._currentPlayTime = new Date(this._startTime)\n this.playbackState = 'playing'\n this._startTimer()\n\n this.dispatchEvent(\n new CustomEvent('playback-start', {\n detail: { fromTime: this._startTime, speed: this.speed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onTogglePlayPause() {\n if (this.playbackState === 'playing') {\n this.playbackState = 'paused'\n this._stopTimer()\n this.dispatchEvent(new CustomEvent('playback-pause', { bubbles: true, composed: true }))\n } else {\n this.playbackState = 'playing'\n this._startTimer()\n this.dispatchEvent(new CustomEvent('playback-resume', { bubbles: true, composed: true }))\n }\n }\n\n private _onStop() {\n this._stopTimer()\n this._currentPlayTime = null\n this._seekValue = 0\n this.playbackState = 'idle'\n this.dispatchEvent(new CustomEvent('playback-stop', { bubbles: true, composed: true }))\n }\n\n private _onSeekInput(e: Event) {\n this._seeking = true\n this._seekValue = Number((e.target as HTMLInputElement).value)\n\n // 드래그 중 preview 시간 표시 (실제 seek은 하지 않음)\n const ratio = this._seekValue / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n this._seekPreviewTime = new Date(from + (to - from) * ratio)\n }\n\n private _onSeekChange(e: Event) {\n this._seeking = false\n const ratio = Number((e.target as HTMLInputElement).value) / 1000\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const targetTime = new Date(from + (to - from) * ratio)\n\n this._seekPreviewTime = null\n this._currentPlayTime = targetTime\n\n this.dispatchEvent(\n new CustomEvent('playback-seek', {\n detail: { toTime: targetTime },\n bubbles: true,\n composed: true\n })\n )\n\n // 재생 중이었으면 새 시점에서 타이머 계속\n if (this.playbackState === 'playing') {\n this._stopTimer()\n this._startTimer()\n }\n }\n\n private _renderBufferBars() {\n if (!this.bufferedRanges?.length) return ''\n\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return ''\n\n return this.bufferedRanges.map(r => {\n const left = Math.max(0, (r.from - from) / range) * 100\n const right = Math.min(1, (r.to - from) / range) * 100\n const width = right - left\n return html`<div class=\"buffer-bar\" style=\"left:${left}%;width:${width}%\"></div>`\n })\n }\n\n private _onSpeedChange(speed: number) {\n this.speed = speed\n\n this.dispatchEvent(\n new CustomEvent('playback-speed', {\n detail: { speed },\n bubbles: true,\n composed: true\n })\n )\n\n // 재생 중이면 타이머 재시작 (새 속도 반영)\n if (this.playbackState === 'playing') {\n this._stopTimer()\n this._startTimer()\n }\n }\n\n // --- 타이머: 초 단위 진행 ---\n\n private _startTimer() {\n this._stopTimer()\n\n const intervalMs = 1000 / this.speed\n this._timer = window.setInterval(() => {\n if (!this._currentPlayTime) return\n\n const next = new Date(this._currentPlayTime.getTime() + 1000)\n\n // 종료 시점 초과 시 정지\n if (next.getTime() > this._endTime.getTime()) {\n this._onStop()\n return\n }\n\n this._currentPlayTime = next\n if (!this._seeking) this._updateSeekFromPlayTime()\n\n this.dispatchEvent(\n new CustomEvent('playback-tick', {\n detail: { currentTime: this._currentPlayTime },\n bubbles: true,\n composed: true\n })\n )\n }, intervalMs)\n }\n\n private _stopTimer() {\n if (this._timer) {\n clearInterval(this._timer)\n this._timer = undefined\n }\n }\n\n private _updateSeekFromPlayTime() {\n if (!this._currentPlayTime) return\n const from = this._startTime.getTime()\n const to = this._endTime.getTime()\n const range = to - from\n if (range <= 0) return\n this._seekValue = Math.max(0, Math.min(1000, Math.round(((this._currentPlayTime.getTime() - from) / range) * 1000)))\n }\n}\n"]}
|