@huyooo/file-explorer-preview 0.4.27 → 0.4.30
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/package.json +14 -25
- package/src/MediaPreviewPage.vue +349 -0
- package/src/components/AudioPlayer.vue +295 -0
- package/src/components/ImageViewer.vue +681 -0
- package/src/components/VideoPlayer.vue +436 -0
- package/src/components/index.ts +3 -0
- package/src/components/ui/PlayerControls.vue +396 -0
- package/src/components/ui/ProgressBar.vue +205 -0
- package/src/components/ui/SpeedMenu.vue +164 -0
- package/src/components/ui/SubtitleMenu.vue +235 -0
- package/src/components/ui/SubtitleOverlay.vue +79 -0
- package/src/components/ui/VolumeControl.vue +235 -0
- package/src/composables/useAutoHideControls.ts +70 -0
- package/src/composables/useFullscreen.ts +64 -0
- package/src/composables/useMediaPlayer.ts +324 -0
- package/src/composables/usePopupMenu.ts +32 -0
- package/src/composables/useSliderDrag.ts +103 -0
- package/src/composables/useSubtitleTracks.ts +78 -0
- package/src/composables/useVideoInteraction.ts +112 -0
- package/src/index.ts +3 -0
- package/src/types/electron.d.ts +16 -0
- package/src/types/previewMediaPreferences.ts +21 -0
- package/src/utils/subtitle.ts +328 -0
- package/src/utils/time.ts +33 -0
- package/src/vite-env.d.ts +8 -0
- package/dist/assets/index-Be3APZtN.js +0 -1
- package/dist/assets/style-DpBBcsI-.css +0 -1
- package/dist/index.html +0 -18
- package/dist/path.d.ts +0 -13
- package/dist/path.js +0 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Transition name="controls-slide">
|
|
3
|
+
<div
|
|
4
|
+
v-if="!hasError"
|
|
5
|
+
v-show="showControls"
|
|
6
|
+
class="controls"
|
|
7
|
+
@pointerenter="handleInteractionStart"
|
|
8
|
+
@pointerleave="handleInteractionEnd"
|
|
9
|
+
@focusin="handleInteractionStart"
|
|
10
|
+
@focusout="handleFocusOut"
|
|
11
|
+
>
|
|
12
|
+
<!-- 第一行:进度条独占,占满宽度 -->
|
|
13
|
+
<div class="progress-row">
|
|
14
|
+
<span class="time time-current">{{ currentTimeFormatted }}</span>
|
|
15
|
+
<ProgressBar
|
|
16
|
+
:played="playedPercent"
|
|
17
|
+
:buffered="bufferedPercent"
|
|
18
|
+
:duration="duration"
|
|
19
|
+
@change="handleProgressChange"
|
|
20
|
+
/>
|
|
21
|
+
<span class="time time-duration">{{ durationFormatted }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- 第二行:按钮组 -->
|
|
25
|
+
<div class="controls-row">
|
|
26
|
+
<div class="left-spacer" />
|
|
27
|
+
<div class="center-controls">
|
|
28
|
+
<button class="btn" @click="handleSeek(-10)" title="后退10秒 (←)">
|
|
29
|
+
<Icon icon="lucide:skip-back" />
|
|
30
|
+
</button>
|
|
31
|
+
<button class="btn btn-play" @click="handleTogglePlay" title="播放/暂停 (空格)">
|
|
32
|
+
<Icon v-if="!isPlaying" icon="lucide:play" />
|
|
33
|
+
<Icon v-else icon="lucide:pause" />
|
|
34
|
+
</button>
|
|
35
|
+
<button class="btn" @click="handleSeek(10)" title="快进10秒 (→)">
|
|
36
|
+
<Icon icon="lucide:skip-forward" />
|
|
37
|
+
</button>
|
|
38
|
+
<button class="btn btn-loop" :class="{ active: loop }" @click="handleToggleLoop" :title="loop ? '取消循环 (L)' : '单曲循环 (L)'">
|
|
39
|
+
<Icon icon="lucide:repeat-1" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="right-controls">
|
|
43
|
+
<VolumeControl
|
|
44
|
+
:volume="volume"
|
|
45
|
+
:muted="isMuted"
|
|
46
|
+
:parent-visible="showControls"
|
|
47
|
+
@update:volume="handleVolumeChange"
|
|
48
|
+
@toggle-mute="handleToggleMute"
|
|
49
|
+
/>
|
|
50
|
+
<SpeedMenu v-model:speed="speedModel" :parent-visible="showControls" />
|
|
51
|
+
<span v-if="showRestoreWindowRatio" class="tooltip-wrapper">
|
|
52
|
+
<button class="btn" @click="handleRestoreWindowRatio" aria-label="恢复视频比例">
|
|
53
|
+
<Icon icon="lucide:scaling" />
|
|
54
|
+
</button>
|
|
55
|
+
<span class="tooltip" role="tooltip">恢复视频比例</span>
|
|
56
|
+
</span>
|
|
57
|
+
<SubtitleMenu
|
|
58
|
+
v-if="showSubtitle"
|
|
59
|
+
:tracks="subtitleTracks || []"
|
|
60
|
+
v-model:current-track="currentTrackModel"
|
|
61
|
+
:parent-visible="showControls"
|
|
62
|
+
@load-subtitle="handleLoadSubtitle"
|
|
63
|
+
/>
|
|
64
|
+
<button v-if="showPiP" class="btn" @click="handleTogglePiP" title="画中画 (P)">
|
|
65
|
+
<Icon icon="lucide:picture-in-picture" />
|
|
66
|
+
</button>
|
|
67
|
+
<button v-if="showFullscreen" class="btn" @click="handleToggleFullscreen" :title="isFullscreen ? '退出全屏 (F)' : '全屏 (F)'">
|
|
68
|
+
<Icon v-if="!isFullscreen" icon="lucide:maximize" />
|
|
69
|
+
<Icon v-else icon="lucide:minimize" />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</Transition>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
import { computed } from 'vue';
|
|
79
|
+
import { Icon } from '@iconify/vue';
|
|
80
|
+
import { formatTime } from '../../utils/time';
|
|
81
|
+
import type { SubtitleTrack } from '../../utils/subtitle';
|
|
82
|
+
import ProgressBar from './ProgressBar.vue';
|
|
83
|
+
import VolumeControl from './VolumeControl.vue';
|
|
84
|
+
import SpeedMenu from './SpeedMenu.vue';
|
|
85
|
+
import SubtitleMenu from './SubtitleMenu.vue';
|
|
86
|
+
|
|
87
|
+
const props = defineProps<{
|
|
88
|
+
/** 是否显示控制器 */
|
|
89
|
+
showControls: boolean;
|
|
90
|
+
/** 是否有错误 */
|
|
91
|
+
hasError: boolean;
|
|
92
|
+
/** 是否正在播放 */
|
|
93
|
+
isPlaying: boolean;
|
|
94
|
+
/** 是否静音 */
|
|
95
|
+
isMuted: boolean;
|
|
96
|
+
/** 当前时间(秒) */
|
|
97
|
+
currentTime: number;
|
|
98
|
+
/** 总时长(秒) */
|
|
99
|
+
duration: number;
|
|
100
|
+
/** 已播放百分比 */
|
|
101
|
+
playedPercent: number;
|
|
102
|
+
/** 已缓冲百分比 */
|
|
103
|
+
bufferedPercent: number;
|
|
104
|
+
/** 音量 */
|
|
105
|
+
volume: number;
|
|
106
|
+
/** 播放速度 */
|
|
107
|
+
playbackRate: number;
|
|
108
|
+
/** 是否全屏 */
|
|
109
|
+
isFullscreen?: boolean;
|
|
110
|
+
/** 是否显示字幕菜单 */
|
|
111
|
+
showSubtitle?: boolean;
|
|
112
|
+
/** 是否显示画中画按钮 */
|
|
113
|
+
showPiP?: boolean;
|
|
114
|
+
/** 是否显示全屏按钮 */
|
|
115
|
+
showFullscreen?: boolean;
|
|
116
|
+
/** 是否显示恢复窗口比例按钮 */
|
|
117
|
+
showRestoreWindowRatio?: boolean;
|
|
118
|
+
/** 是否循环播放 */
|
|
119
|
+
loop?: boolean;
|
|
120
|
+
/** 字幕轨道列表 */
|
|
121
|
+
subtitleTracks?: SubtitleTrack[];
|
|
122
|
+
/** 当前选中的字幕轨道索引 */
|
|
123
|
+
currentTrackIndex?: number;
|
|
124
|
+
}>();
|
|
125
|
+
|
|
126
|
+
const emit = defineEmits<{
|
|
127
|
+
/** 切换播放/暂停 */
|
|
128
|
+
togglePlay: [];
|
|
129
|
+
/** 快进/快退 */
|
|
130
|
+
seek: [seconds: number];
|
|
131
|
+
/** 进度条变化 */
|
|
132
|
+
progressChange: [percent: number];
|
|
133
|
+
/** 音量变化 */
|
|
134
|
+
volumeChange: [volume: number];
|
|
135
|
+
/** 切换静音 */
|
|
136
|
+
toggleMute: [];
|
|
137
|
+
/** 速度变化 */
|
|
138
|
+
speedChange: [speed: number];
|
|
139
|
+
/** 切换画中画 */
|
|
140
|
+
togglePiP: [];
|
|
141
|
+
/** 切换全屏 */
|
|
142
|
+
toggleFullscreen: [];
|
|
143
|
+
/** 切换循环 */
|
|
144
|
+
toggleLoop: [];
|
|
145
|
+
/** 恢复窗口比例 */
|
|
146
|
+
restoreWindowRatio: [];
|
|
147
|
+
/** 切换字幕轨道 */
|
|
148
|
+
trackChange: [index: number];
|
|
149
|
+
/** 加载字幕文件 */
|
|
150
|
+
loadSubtitle: [];
|
|
151
|
+
/** 用户正在和控制器交互 */
|
|
152
|
+
controlsInteractionStart: [];
|
|
153
|
+
/** 用户结束和控制器交互 */
|
|
154
|
+
controlsInteractionEnd: [];
|
|
155
|
+
}>();
|
|
156
|
+
|
|
157
|
+
/** 是否按小时格式显示(与总时长对齐,避免进度条宽度跳动) */
|
|
158
|
+
const useHoursFormat = computed(() => Number.isFinite(props.duration) && props.duration >= 3600);
|
|
159
|
+
|
|
160
|
+
/** 格式化的时间显示 */
|
|
161
|
+
const currentTimeFormatted = computed(() => formatTime(props.currentTime, useHoursFormat.value));
|
|
162
|
+
const durationFormatted = computed(() => formatTime(props.duration, useHoursFormat.value));
|
|
163
|
+
const speedModel = computed({
|
|
164
|
+
get: () => props.playbackRate,
|
|
165
|
+
set: (speed: number) => emit('speedChange', speed),
|
|
166
|
+
});
|
|
167
|
+
const currentTrackModel = computed({
|
|
168
|
+
get: () => props.currentTrackIndex ?? -1,
|
|
169
|
+
set: (index: number) => emit('trackChange', index),
|
|
170
|
+
});
|
|
171
|
+
/** 处理播放/暂停 */
|
|
172
|
+
function handleTogglePlay() {
|
|
173
|
+
emit('togglePlay');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 处理快进/快退 */
|
|
177
|
+
function handleSeek(seconds: number) {
|
|
178
|
+
emit('seek', seconds);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 处理进度条变化 */
|
|
182
|
+
function handleProgressChange(percent: number) {
|
|
183
|
+
emit('progressChange', percent);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 处理音量变化 */
|
|
187
|
+
function handleVolumeChange(volume: number) {
|
|
188
|
+
emit('volumeChange', volume);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 处理切换静音 */
|
|
192
|
+
function handleToggleMute() {
|
|
193
|
+
emit('toggleMute');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 处理切换画中画 */
|
|
197
|
+
function handleTogglePiP() {
|
|
198
|
+
emit('togglePiP');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 处理切换全屏 */
|
|
202
|
+
function handleToggleFullscreen() {
|
|
203
|
+
emit('toggleFullscreen');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 处理切换循环 */
|
|
207
|
+
function handleToggleLoop() {
|
|
208
|
+
emit('toggleLoop');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleRestoreWindowRatio() {
|
|
212
|
+
emit('restoreWindowRatio');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 处理加载字幕 */
|
|
216
|
+
function handleLoadSubtitle() {
|
|
217
|
+
emit('loadSubtitle');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleInteractionStart() {
|
|
221
|
+
emit('controlsInteractionStart');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleInteractionEnd() {
|
|
225
|
+
emit('controlsInteractionEnd');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleFocusOut(event: FocusEvent) {
|
|
229
|
+
const next = event.relatedTarget;
|
|
230
|
+
if (next instanceof Node && event.currentTarget instanceof Node && event.currentTarget.contains(next)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
handleInteractionEnd();
|
|
234
|
+
}
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<style scoped>
|
|
238
|
+
/* 控制栏 - 毛玻璃效果 */
|
|
239
|
+
.controls {
|
|
240
|
+
position: absolute;
|
|
241
|
+
bottom: 16px;
|
|
242
|
+
left: 16px;
|
|
243
|
+
right: 16px;
|
|
244
|
+
background: var(--huyooo-panel-bg);
|
|
245
|
+
backdrop-filter: blur(20px);
|
|
246
|
+
border-radius: 12px;
|
|
247
|
+
border: 1px solid var(--huyooo-border);
|
|
248
|
+
padding: 12px 16px;
|
|
249
|
+
box-shadow: var(--huyooo-shadow-lg);
|
|
250
|
+
-webkit-app-region: no-drag;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* 控制栏滑入滑出动画 */
|
|
254
|
+
.controls-slide-enter-active,
|
|
255
|
+
.controls-slide-leave-active {
|
|
256
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
257
|
+
}
|
|
258
|
+
.controls-slide-enter-from,
|
|
259
|
+
.controls-slide-leave-to {
|
|
260
|
+
opacity: 0;
|
|
261
|
+
transform: translateY(12px);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* 第一行:进度条独占,时间在两侧 */
|
|
265
|
+
.progress-row {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 10px;
|
|
269
|
+
min-width: 0;
|
|
270
|
+
margin-bottom: 10px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.progress-row :deep(.progress-container) {
|
|
274
|
+
flex: 1;
|
|
275
|
+
min-width: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* 第二行:左空白 | 播放控制居中 | 右侧工具 */
|
|
279
|
+
.controls-row {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
gap: 12px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.left-spacer {
|
|
287
|
+
flex: 1;
|
|
288
|
+
min-width: 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.center-controls {
|
|
292
|
+
display: flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
gap: 2px;
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.right-controls {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
gap: 2px;
|
|
302
|
+
flex-shrink: 0;
|
|
303
|
+
flex: 1;
|
|
304
|
+
justify-content: flex-end;
|
|
305
|
+
min-width: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.tooltip-wrapper {
|
|
309
|
+
position: relative;
|
|
310
|
+
display: inline-flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.tooltip {
|
|
315
|
+
position: absolute;
|
|
316
|
+
right: 50%;
|
|
317
|
+
bottom: calc(100% + 8px);
|
|
318
|
+
transform: translateX(50%) translateY(4px);
|
|
319
|
+
padding: 6px 8px;
|
|
320
|
+
border-radius: 6px;
|
|
321
|
+
background: color-mix(in srgb, #111 90%, transparent);
|
|
322
|
+
color: #fff;
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
line-height: 1;
|
|
325
|
+
white-space: nowrap;
|
|
326
|
+
box-shadow: var(--huyooo-shadow-lg);
|
|
327
|
+
opacity: 0;
|
|
328
|
+
pointer-events: none;
|
|
329
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
330
|
+
z-index: 10;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.tooltip-wrapper:hover .tooltip,
|
|
334
|
+
.tooltip-wrapper:focus-within .tooltip {
|
|
335
|
+
opacity: 1;
|
|
336
|
+
transform: translateX(50%) translateY(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.btn {
|
|
340
|
+
width: 32px;
|
|
341
|
+
height: 32px;
|
|
342
|
+
background: transparent;
|
|
343
|
+
border: none;
|
|
344
|
+
border-radius: 6px;
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
justify-content: center;
|
|
349
|
+
color: var(--huyooo-text);
|
|
350
|
+
transition: all 0.15s;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.btn:hover {
|
|
354
|
+
background: var(--huyooo-muted-hover);
|
|
355
|
+
color: var(--huyooo-on-primary, #ffffff);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.btn:active {
|
|
359
|
+
transform: scale(0.95);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.btn :deep(.iconify) {
|
|
363
|
+
font-size: 18px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.btn-play {
|
|
367
|
+
width: 36px;
|
|
368
|
+
height: 36px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.btn-play :deep(.iconify) {
|
|
372
|
+
font-size: 20px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.btn.active {
|
|
376
|
+
color: var(--huyooo-primary);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* 时间显示:等宽数字,固定最小宽度避免跳动 */
|
|
380
|
+
.time {
|
|
381
|
+
color: var(--huyooo-text-muted);
|
|
382
|
+
font-size: 12px;
|
|
383
|
+
font-weight: 500;
|
|
384
|
+
font-variant-numeric: tabular-nums;
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
min-width: 2.8em;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.time-current {
|
|
390
|
+
text-align: right;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.time-duration {
|
|
394
|
+
text-align: left;
|
|
395
|
+
}
|
|
396
|
+
</style>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import { useSliderDrag } from '../../composables/useSliderDrag';
|
|
4
|
+
import { formatTime } from '../../utils/time';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
/** 已播放百分比 (0-100) */
|
|
8
|
+
played: number;
|
|
9
|
+
/** 已缓冲百分比 (0-100) */
|
|
10
|
+
buffered?: number;
|
|
11
|
+
/** 总时长(秒),用于悬停预览 */
|
|
12
|
+
duration?: number;
|
|
13
|
+
/** 主色调 */
|
|
14
|
+
color?: string;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
/** 进度变化事件,参数为百分比 (0-1) */
|
|
19
|
+
change: [percent: number];
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const containerRef = ref<HTMLElement>();
|
|
23
|
+
|
|
24
|
+
const { isDragging, handleMouseDown, handleTouchStart } = useSliderDrag({
|
|
25
|
+
getElement: () => containerRef.value,
|
|
26
|
+
onChange: (percent) => emit('change', percent),
|
|
27
|
+
thumbRadius: 7
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const progressColor = computed(() => props.color || 'var(--huyooo-primary)');
|
|
31
|
+
|
|
32
|
+
/** 悬停预览:根据 x 计算百分比 */
|
|
33
|
+
function calcHoverPercent(clientX: number): number {
|
|
34
|
+
const el = containerRef.value;
|
|
35
|
+
if (!el) return 0;
|
|
36
|
+
const rect = el.getBoundingClientRect();
|
|
37
|
+
const thumbRadius = 7;
|
|
38
|
+
const trackWidth = rect.width - thumbRadius * 2;
|
|
39
|
+
const offsetX = clientX - rect.left - thumbRadius;
|
|
40
|
+
return Math.max(0, Math.min(1, offsetX / trackWidth));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const showPreview = ref(false);
|
|
44
|
+
const previewTime = ref(0);
|
|
45
|
+
const previewPercent = ref(0);
|
|
46
|
+
|
|
47
|
+
function updatePreview(clientX: number) {
|
|
48
|
+
const dur = props.duration;
|
|
49
|
+
if (dur == null || !Number.isFinite(dur) || dur <= 0) return;
|
|
50
|
+
const percent = calcHoverPercent(clientX);
|
|
51
|
+
showPreview.value = true;
|
|
52
|
+
previewPercent.value = percent;
|
|
53
|
+
previewTime.value = percent * dur;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleMouseMove(e: MouseEvent) {
|
|
57
|
+
updatePreview(e.clientX);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleTouchMove(e: TouchEvent) {
|
|
61
|
+
if (e.touches.length > 0) updatePreview(e.touches[0].clientX);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleContainerTouchStart(e: TouchEvent) {
|
|
65
|
+
if (e.touches.length > 0) updatePreview(e.touches[0].clientX);
|
|
66
|
+
handleTouchStart(e);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handlePointerLeave() {
|
|
70
|
+
showPreview.value = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const previewTimeFormatted = computed(() => formatTime(previewTime.value));
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div
|
|
78
|
+
ref="containerRef"
|
|
79
|
+
class="progress-container"
|
|
80
|
+
:class="{ dragging: isDragging }"
|
|
81
|
+
@mousedown="handleMouseDown"
|
|
82
|
+
@touchstart="handleContainerTouchStart"
|
|
83
|
+
@mousemove="handleMouseMove"
|
|
84
|
+
@touchmove="handleTouchMove"
|
|
85
|
+
@mouseleave="handlePointerLeave"
|
|
86
|
+
@touchend="handlePointerLeave"
|
|
87
|
+
>
|
|
88
|
+
<Transition name="fade">
|
|
89
|
+
<div
|
|
90
|
+
v-if="showPreview && duration != null && Number.isFinite(duration) && duration > 0"
|
|
91
|
+
class="progress-preview"
|
|
92
|
+
:style="{ left: previewPercent * 100 + '%' }"
|
|
93
|
+
>
|
|
94
|
+
{{ previewTimeFormatted }}
|
|
95
|
+
</div>
|
|
96
|
+
</Transition>
|
|
97
|
+
<div class="progress-bar">
|
|
98
|
+
<div
|
|
99
|
+
v-if="buffered !== undefined"
|
|
100
|
+
class="progress-buffered"
|
|
101
|
+
:style="{ width: buffered + '%' }"
|
|
102
|
+
/>
|
|
103
|
+
<div
|
|
104
|
+
class="progress-played"
|
|
105
|
+
:style="{ width: played + '%', backgroundColor: progressColor }"
|
|
106
|
+
/>
|
|
107
|
+
<div
|
|
108
|
+
class="progress-thumb"
|
|
109
|
+
:style="{ left: played + '%' }"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.progress-container {
|
|
117
|
+
flex: 1;
|
|
118
|
+
min-width: 0;
|
|
119
|
+
height: 16px;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
padding: 0 6px;
|
|
124
|
+
margin-left: -6px;
|
|
125
|
+
margin-right: -6px;
|
|
126
|
+
user-select: none;
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.progress-preview {
|
|
131
|
+
position: absolute;
|
|
132
|
+
bottom: 100%;
|
|
133
|
+
transform: translate(-50%, -6px);
|
|
134
|
+
padding: 2px 6px;
|
|
135
|
+
font-size: 11px;
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
font-variant-numeric: tabular-nums;
|
|
138
|
+
color: var(--huyooo-on-primary, #fff);
|
|
139
|
+
background: rgba(0, 0, 0, 0.8);
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.fade-enter-active,
|
|
146
|
+
.fade-leave-active {
|
|
147
|
+
transition: opacity 0.1s;
|
|
148
|
+
}
|
|
149
|
+
.fade-enter-from,
|
|
150
|
+
.fade-leave-to {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.progress-bar {
|
|
155
|
+
flex: 1;
|
|
156
|
+
height: 3px;
|
|
157
|
+
background: color-mix(in srgb, var(--huyooo-text) 15%, transparent);
|
|
158
|
+
border-radius: 2px;
|
|
159
|
+
position: relative;
|
|
160
|
+
transition: height 0.15s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.progress-container:hover .progress-bar,
|
|
164
|
+
.progress-container.dragging .progress-bar {
|
|
165
|
+
height: 5px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.progress-buffered {
|
|
169
|
+
position: absolute;
|
|
170
|
+
left: 0;
|
|
171
|
+
top: 0;
|
|
172
|
+
height: 100%;
|
|
173
|
+
background: color-mix(in srgb, var(--huyooo-text) 25%, transparent);
|
|
174
|
+
border-radius: 2px;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.progress-played {
|
|
179
|
+
position: absolute;
|
|
180
|
+
left: 0;
|
|
181
|
+
top: 0;
|
|
182
|
+
height: 100%;
|
|
183
|
+
border-radius: 2px;
|
|
184
|
+
pointer-events: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.progress-thumb {
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: 50%;
|
|
190
|
+
width: 12px;
|
|
191
|
+
height: 12px;
|
|
192
|
+
background: var(--huyooo-on-primary, #fff);
|
|
193
|
+
border-radius: 50%;
|
|
194
|
+
transform: translate(-50%, -50%) scale(0);
|
|
195
|
+
transition: transform 0.15s;
|
|
196
|
+
box-shadow: var(--huyooo-shadow-sm);
|
|
197
|
+
pointer-events: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.progress-container:hover .progress-thumb,
|
|
201
|
+
.progress-container.dragging .progress-thumb {
|
|
202
|
+
transform: translate(-50%, -50%) scale(1);
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
205
|
+
|