lzc-video-player 0.0.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.
- package/.dockerignore +1 -0
- package/.eslintrc.cjs +18 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +31 -0
- package/README.md +38 -0
- package/build.sh +10 -0
- package/demo/.vscode/extensions.json +3 -0
- package/demo/README.md +40 -0
- package/demo/env.d.ts +1 -0
- package/demo/index.html +13 -0
- package/demo/package-lock.json +2037 -0
- package/demo/package.json +25 -0
- package/demo/public/favicon.ico +0 -0
- package/demo/src/App.vue +25 -0
- package/demo/src/assets/base.css +70 -0
- package/demo/src/assets/logo.svg +1 -0
- package/demo/src/assets/main.css +33 -0
- package/demo/src/main.ts +8 -0
- package/demo/tsconfig.config.json +8 -0
- package/demo/tsconfig.json +16 -0
- package/demo/vite.config.ts +14 -0
- package/docs/progress-bar-style-analysis.md +87 -0
- package/env.d.ts +1 -0
- package/error_pages/502.html.tpl +13 -0
- package/i18next-parser.config.mjs +147 -0
- package/index.html +54 -0
- package/lazycat.png +0 -0
- package/lib/README.md +48 -0
- package/lib/package.json +22 -0
- package/lzc-build.local.yml +65 -0
- package/lzc-build.yml +65 -0
- package/lzc-manifest.yml +53 -0
- package/makefile +15 -0
- package/package.json +69 -0
- package/postcss.config.js +6 -0
- package/public/512x512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/languages/en/translation.json +125 -0
- package/public/languages/zh/translation.json +125 -0
- package/public/libass-wasm/4.1.0/default.woff2 +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker-legacy.js +40 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.js +1 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.wasm +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus.js +1680 -0
- package/public/square-128x128.png +0 -0
- package/public/square-256x256.png +0 -0
- package/public/square-512x512.png +0 -0
- package/src/App.vue +18 -0
- package/src/assets/base.scss +104 -0
- package/src/assets/cloud.png +0 -0
- package/src/assets/logo.svg +1 -0
- package/src/components/Dialog/index.vue +96 -0
- package/src/components/MultipleEdit/choose.vue +39 -0
- package/src/components/PlayList/index.vue +521 -0
- package/src/components/Spectrum/index.vue +58 -0
- package/src/components/Video/NativeVideoPlayer.vue +748 -0
- package/src/components/Video/README.md +3 -0
- package/src/components/Video/clientPlayer.ts +348 -0
- package/src/components/Video/components/LzcModal/components/simpleList.vue +57 -0
- package/src/components/Video/components/LzcModal/list.vue +52 -0
- package/src/components/Video/components/LzcModal/playrate.vue +45 -0
- package/src/components/Video/components/LzcModal/resolution.vue +117 -0
- package/src/components/Video/components/LzcModal/subtitle.vue +499 -0
- package/src/components/Video/components/LzcModal/useModal.ts +18 -0
- package/src/components/Video/components/LzcOverlay/SubtitleLayer.vue +321 -0
- package/src/components/Video/components/LzcOverlay/cast.vue +253 -0
- package/src/components/Video/components/LzcOverlay/casting.vue +205 -0
- package/src/components/Video/components/LzcOverlay/error.vue +103 -0
- package/src/components/Video/components/LzcOverlay/helper.ts +81 -0
- package/src/components/Video/components/LzcOverlay/index.vue +99 -0
- package/src/components/Video/components/LzcOverlay/playing.vue +496 -0
- package/src/components/Video/components/LzcOverlay/playingButtons.vue +122 -0
- package/src/components/Video/components/LzcOverlay/playingLayout.vue +287 -0
- package/src/components/Video/components/LzcOverlay/useCast.ts +235 -0
- package/src/components/Video/components/LzcOverlay/useCommon.ts +41 -0
- package/src/components/Video/components/LzcOverlay/useOctopusRenderer.ts +230 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleRenderEngine.ts +79 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleTrack.ts +139 -0
- package/src/components/Video/components/useLzcCommon.ts +16 -0
- package/src/components/Video/directPlay.ts +345 -0
- package/src/components/Video/getSubtitleInfo.ts +42 -0
- package/src/components/Video/native/EventEmitter.ts +62 -0
- package/src/components/Video/native/NativeControls.vue +510 -0
- package/src/components/Video/native/NativeModal.vue +133 -0
- package/src/components/Video/native/NativePlayer.ts +913 -0
- package/src/components/Video/native/NativePlayer.vue +53 -0
- package/src/components/Video/native/index.ts +9 -0
- package/src/components/Video/native/native-player.css +183 -0
- package/src/components/Video/native/playerKey.ts +5 -0
- package/src/components/Video/native/useNativeCastMiddleware.ts +50 -0
- package/src/components/Video/native/useNativePlayer.ts +3 -0
- package/src/components/Video/native/useNativePlayerFullscreen.ts +44 -0
- package/src/components/Video/native/useNativePlayerHistory.ts +69 -0
- package/src/components/Video/native/useNativePlayerModal.ts +68 -0
- package/src/components/Video/native/useNativePlayerPlaylist.ts +67 -0
- package/src/components/Video/native/useNativePlayerState.ts +225 -0
- package/src/components/Video/player.ts +99 -0
- package/src/components/Video/theme/index.scss +291 -0
- package/src/components/Video/theme/videojs.css +1797 -0
- package/src/components/Video/useSource.ts +1431 -0
- package/src/components/Video/useSubtitlePreference.ts +66 -0
- package/src/components/Video/useWebview.ts +79 -0
- package/src/components/Video/videoFrame.ts +58 -0
- package/src/env.d.ts +3 -0
- package/src/i18n/README.md +392 -0
- package/src/i18n/index.ts +49 -0
- package/src/icons/Video_Player.svg +69 -0
- package/src/icons/box.svg +15 -0
- package/src/icons/client.svg +17 -0
- package/src/icons/logo.svg +28 -0
- package/src/icons//344/270/212/344/270/200/344/270/252.svg +6 -0
- package/src/icons//344/270/213/344/270/200/344/270/252.svg +4 -0
- package/src/icons//344/272/256/345/272/246.svg +13 -0
- package/src/icons//345/200/215/351/200/237.svg +14 -0
- package/src/icons//345/205/250/345/261/217.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/345/267/262/351/200/211/344/270/255.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/346/234/252/351/200/211/344/270/255.svg +15 -0
- package/src/icons//345/205/263/351/227/255/345/244/232/351/200/211.svg +14 -0
- package/src/icons//345/205/263/351/227/255/346/212/225/345/261/217.svg +11 -0
- package/src/icons//345/233/236/346/224/266/347/253/231.svg +15 -0
- package/src/icons//345/244/261/346/225/210.svg +17 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250-icon.png +0 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250.png +0 -0
- package/src/icons//346/212/225/345/261/217.svg +11 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.jpg +0 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.svg +21 -0
- package/src/icons//346/222/255/346/224/276.svg +3 -0
- package/src/icons//346/232/202/345/201/234.svg +4 -0
- package/src/icons//346/232/202/346/227/240.svg +21 -0
- package/src/icons//346/233/264/345/244/232/346/223/215/344/275/234.svg +11 -0
- package/src/icons//347/224/265/350/247/206.svg +18 -0
- package/src/icons//347/247/273/345/212/250/347/253/257_/350/203/214/346/231/257.webp +0 -0
- package/src/icons//350/203/214/346/231/257.png +0 -0
- package/src/icons//350/277/224/345/233/236.svg +13 -0
- package/src/icons//350/277/233/345/205/245/345/205/250/345/261/217.svg +13 -0
- package/src/icons//351/200/200/345/207/272/345/205/250/345/261/217.svg +15 -0
- package/src/icons//351/200/211/346/213/251.svg +15 -0
- package/src/icons//351/237/263/351/207/217.svg +13 -0
- package/src/index.d.ts +9 -0
- package/src/lzc-video-player.scss +7 -0
- package/src/lzc-video-player.ts +6 -0
- package/src/main.ts +62 -0
- package/src/model.ts +77 -0
- package/src/quasar-variables.sass +10 -0
- package/src/router/index.ts +74 -0
- package/src/stores/pinia.ts +3 -0
- package/src/stores/playlist.ts +146 -0
- package/src/use/useKeyBind.ts +61 -0
- package/src/use/useMultipleEdit.ts +60 -0
- package/src/use/useSdk.ts +5 -0
- package/src/use/useSubtitle.ts +39 -0
- package/src/use/useUtils.ts +22 -0
- package/src/use/useVideoFrame.ts +60 -0
- package/src/views/Home.ts +99 -0
- package/src/views/mobile/Home.vue +246 -0
- package/src/views/mobile/Player.vue +141 -0
- package/tailwind.config.js +15 -0
- package/tsconfig.config.json +8 -0
- package/tsconfig.json +20 -0
- package/vite.config.lib.ts +88 -0
- package/vite.config.ts +122 -0
- package/vue-shim.d.ts +4 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted, watch, inject } from "vue"
|
|
3
|
+
import debounce from "lodash.debounce"
|
|
4
|
+
import type { NativePlayerAPI } from "./NativePlayer"
|
|
5
|
+
import { nativePlayerKey } from "./playerKey"
|
|
6
|
+
import { useHistoryInfo } from "@/stores/playlist"
|
|
7
|
+
import { isSourceEqual } from "@/use/useUtils"
|
|
8
|
+
import { t } from "@/i18n"
|
|
9
|
+
import { isMobile } from "@/use/useUtils"
|
|
10
|
+
import PrevSvg from "@/icons/上一个.svg?inline"
|
|
11
|
+
import NextSvg from "@/icons/下一个.svg?inline"
|
|
12
|
+
import PlaySvg from "@/icons/播放.svg?inline"
|
|
13
|
+
import PausedSvg from "@/icons/暂停.svg?inline"
|
|
14
|
+
import FullscreenSvg from "@/icons/进入全屏.svg?inline"
|
|
15
|
+
import ExitFullscreenSvg from "@/icons/退出全屏.svg?inline"
|
|
16
|
+
|
|
17
|
+
const playerRef = inject(nativePlayerKey, ref<NativePlayerAPI | null>(null))
|
|
18
|
+
const player = computed(() => playerRef.value)
|
|
19
|
+
const store = useHistoryInfo()
|
|
20
|
+
const fullscreenEnabled =
|
|
21
|
+
typeof document !== "undefined" && document.fullscreenEnabled
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(
|
|
24
|
+
defineProps<{
|
|
25
|
+
currentTime?: number
|
|
26
|
+
duration?: number
|
|
27
|
+
isPlaying?: boolean
|
|
28
|
+
isFullscreen?: boolean
|
|
29
|
+
prevEnabled?: boolean
|
|
30
|
+
nextEnabled?: boolean
|
|
31
|
+
hidePrevButton?: boolean
|
|
32
|
+
hideNextButton?: boolean
|
|
33
|
+
hidePlayList?: boolean
|
|
34
|
+
hideSubtitle?: boolean
|
|
35
|
+
hideResolution?: boolean
|
|
36
|
+
}>(),
|
|
37
|
+
{
|
|
38
|
+
currentTime: 0,
|
|
39
|
+
duration: 0,
|
|
40
|
+
isPlaying: true,
|
|
41
|
+
isFullscreen: false,
|
|
42
|
+
prevEnabled: false,
|
|
43
|
+
nextEnabled: false,
|
|
44
|
+
hidePrevButton: false,
|
|
45
|
+
hideNextButton: false,
|
|
46
|
+
hidePlayList: false,
|
|
47
|
+
hideSubtitle: false,
|
|
48
|
+
hideResolution: false,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits<{
|
|
53
|
+
(e: "open-modal", page: string, ev: Event): void
|
|
54
|
+
}>()
|
|
55
|
+
|
|
56
|
+
const seeking = ref(false)
|
|
57
|
+
const seekValue = ref(0)
|
|
58
|
+
|
|
59
|
+
let rateQualityOff: (() => void) | null = null
|
|
60
|
+
|
|
61
|
+
function clampPercent(value: number): number {
|
|
62
|
+
if (!Number.isFinite(value)) return 0
|
|
63
|
+
return Math.min(100, Math.max(0, value))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const progressPercent = computed(() => {
|
|
67
|
+
const d = props.duration
|
|
68
|
+
if (d <= 0) return 0
|
|
69
|
+
return clampPercent((props.currentTime / d) * 100)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const displayProgressPercent = computed(() => {
|
|
73
|
+
const d = props.duration
|
|
74
|
+
if (seeking.value && d > 0) {
|
|
75
|
+
return clampPercent((seekValue.value / d) * 100)
|
|
76
|
+
}
|
|
77
|
+
return progressPercent.value
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const displayCurrentTime = computed(() =>
|
|
81
|
+
seeking.value ? seekValue.value : props.currentTime,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const playbackRateText = ref("倍速")
|
|
85
|
+
const resolutionText = ref("清晰度")
|
|
86
|
+
|
|
87
|
+
function togglePlay() {
|
|
88
|
+
const p = player.value
|
|
89
|
+
if (!p) return
|
|
90
|
+
if (p.paused()) {
|
|
91
|
+
p.play()
|
|
92
|
+
} else {
|
|
93
|
+
p.pause()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const progressTrackRef = ref<HTMLElement | null>(null)
|
|
98
|
+
const dragging = ref(false)
|
|
99
|
+
const hoverActive = ref(false)
|
|
100
|
+
const hoverPercent = ref(0)
|
|
101
|
+
|
|
102
|
+
const hoverTime = computed(() => {
|
|
103
|
+
const d = props.duration
|
|
104
|
+
if (d <= 0) return 0
|
|
105
|
+
const seconds = hoverPercent.value * d
|
|
106
|
+
return Math.max(0, Math.min(d, seconds))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
function isIosPortraitFakeFullscreen(): boolean {
|
|
110
|
+
const p = player.value
|
|
111
|
+
if (!p?.isFullscreen()) return false
|
|
112
|
+
return p.hasClass("vjs-ios-fake-fullscreen-portrait")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getPercentFromClientPoint(clientX: number, clientY: number): number {
|
|
116
|
+
const el = progressTrackRef.value
|
|
117
|
+
if (!el) return 0
|
|
118
|
+
const rect = el.getBoundingClientRect()
|
|
119
|
+
let pct = 0
|
|
120
|
+
if (isIosPortraitFakeFullscreen()) {
|
|
121
|
+
if (rect.height <= 0) return 0
|
|
122
|
+
// In portrait fake fullscreen, container rotates 90deg.
|
|
123
|
+
// Screen Y axis maps to timeline direction (top -> start, bottom -> end).
|
|
124
|
+
pct = (clientY - rect.top) / rect.height
|
|
125
|
+
} else {
|
|
126
|
+
if (rect.width <= 0) return 0
|
|
127
|
+
pct = (clientX - rect.left) / rect.width
|
|
128
|
+
}
|
|
129
|
+
pct = Math.max(0, Math.min(1, pct))
|
|
130
|
+
return pct
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isTouchLikeEvent(
|
|
134
|
+
e: MouseEvent | TouchEvent,
|
|
135
|
+
): e is TouchEvent {
|
|
136
|
+
return "touches" in e || "changedTouches" in e
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getPercentFromEvent(e: MouseEvent | TouchEvent): number {
|
|
140
|
+
if (isTouchLikeEvent(e)) {
|
|
141
|
+
const t = e.touches[0] ?? e.changedTouches[0]
|
|
142
|
+
return getPercentFromClientPoint(t?.clientX ?? 0, t?.clientY ?? 0)
|
|
143
|
+
}
|
|
144
|
+
return getPercentFromClientPoint(e.clientX, e.clientY)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onProgressMouseEnter(e: MouseEvent) {
|
|
148
|
+
if (isMobile()) return
|
|
149
|
+
hoverActive.value = true
|
|
150
|
+
hoverPercent.value = getPercentFromClientPoint(e.clientX, e.clientY)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onProgressMouseMove(e: MouseEvent) {
|
|
154
|
+
if (isMobile()) return
|
|
155
|
+
hoverActive.value = true
|
|
156
|
+
hoverPercent.value = getPercentFromClientPoint(e.clientX, e.clientY)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onProgressMouseLeave() {
|
|
160
|
+
hoverActive.value = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applySeek(pct: number) {
|
|
164
|
+
seeking.value = true
|
|
165
|
+
const d = props.duration || 1
|
|
166
|
+
seekValue.value = pct * d
|
|
167
|
+
const p = player.value
|
|
168
|
+
if (p) p.currentTime(seekValue.value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onProgressPointerDown(e: MouseEvent | TouchEvent) {
|
|
172
|
+
e.preventDefault()
|
|
173
|
+
dragging.value = true
|
|
174
|
+
const p = player.value
|
|
175
|
+
if (p) {
|
|
176
|
+
p.setScrubbing(true)
|
|
177
|
+
p.trigger({ type: "lzcSeekBarDown" })
|
|
178
|
+
}
|
|
179
|
+
const pct = getPercentFromEvent(e)
|
|
180
|
+
applySeek(pct)
|
|
181
|
+
if (e instanceof MouseEvent && !isMobile()) {
|
|
182
|
+
hoverActive.value = true
|
|
183
|
+
hoverPercent.value = pct
|
|
184
|
+
}
|
|
185
|
+
const onMove = (ev: MouseEvent | TouchEvent) => {
|
|
186
|
+
const p = getPercentFromEvent(ev)
|
|
187
|
+
applySeek(p)
|
|
188
|
+
if (ev instanceof MouseEvent && !isMobile()) {
|
|
189
|
+
hoverActive.value = true
|
|
190
|
+
hoverPercent.value = p
|
|
191
|
+
}
|
|
192
|
+
if (player.value) {
|
|
193
|
+
player.value.trigger({ type: "lzcSeekBarMove" })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const onUp = () => {
|
|
197
|
+
onSeekEnd()
|
|
198
|
+
document.removeEventListener("mousemove", onMove as (e: MouseEvent) => void)
|
|
199
|
+
document.removeEventListener("mouseup", onUp)
|
|
200
|
+
document.removeEventListener("touchmove", onMove as (e: TouchEvent) => void)
|
|
201
|
+
document.removeEventListener("touchend", onUp)
|
|
202
|
+
}
|
|
203
|
+
document.addEventListener("mousemove", onMove as (e: MouseEvent) => void)
|
|
204
|
+
document.addEventListener("mouseup", onUp)
|
|
205
|
+
document.addEventListener("touchmove", onMove as (e: TouchEvent) => void, {
|
|
206
|
+
passive: false,
|
|
207
|
+
})
|
|
208
|
+
document.addEventListener("touchend", onUp)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onSeekEnd() {
|
|
212
|
+
dragging.value = false
|
|
213
|
+
const p = player.value
|
|
214
|
+
if (p) {
|
|
215
|
+
p.setScrubbing(false)
|
|
216
|
+
p.trigger({ type: "lzcSeekBarUp" })
|
|
217
|
+
}
|
|
218
|
+
hoverActive.value = false
|
|
219
|
+
// 不在此处设置 seeking=false,等 seeked 事件后再清除,避免进度点先跳过去又回弹
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onSeeked() {
|
|
223
|
+
seeking.value = false
|
|
224
|
+
const p = player.value
|
|
225
|
+
if (p) p.setScrubbing(false)
|
|
226
|
+
if (p) seekValue.value = p.currentTime() ?? 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function subscribeRateQuality(p: NativePlayerAPI) {
|
|
230
|
+
const onRatechange = () => {
|
|
231
|
+
const r = p.playbackRate()
|
|
232
|
+
playbackRateText.value =
|
|
233
|
+
r !== 1
|
|
234
|
+
? `${r} x`
|
|
235
|
+
: t(
|
|
236
|
+
"src.components.video.components.lzc_play_rate.speed_btn_text_2",
|
|
237
|
+
"倍速",
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
const updateResolutionText = () => {
|
|
241
|
+
const castMode = (p as { isCastMode?: () => boolean }).isCastMode?.()
|
|
242
|
+
if (castMode) {
|
|
243
|
+
resolutionText.value = t(
|
|
244
|
+
"src.components.video.components.lzc_resolution_button.definition_btn_text_2",
|
|
245
|
+
"清晰度",
|
|
246
|
+
)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
const resolution = p.currentResolution()
|
|
250
|
+
if (resolution == undefined) {
|
|
251
|
+
resolutionText.value = t(
|
|
252
|
+
"src.components.video.components.lzc_resolution_button.definition_btn_text_2",
|
|
253
|
+
"清晰度",
|
|
254
|
+
)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
let text: string
|
|
258
|
+
if (resolution.origin === true) {
|
|
259
|
+
text = t(
|
|
260
|
+
"src.components.video.components.lzc_resolution_button.original_quality_btn_text",
|
|
261
|
+
"原始画质",
|
|
262
|
+
)
|
|
263
|
+
} else {
|
|
264
|
+
text = `${resolution.res} P`
|
|
265
|
+
}
|
|
266
|
+
if (resolution.auto === true) {
|
|
267
|
+
text = t(
|
|
268
|
+
"src.components.video.components.lzc_resolution_button.auto_quality_btn_text",
|
|
269
|
+
"自动({{text}})",
|
|
270
|
+
{ text },
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
resolutionText.value = text
|
|
274
|
+
}
|
|
275
|
+
const debouncedUpdateResolution = debounce(updateResolutionText, 1000, {
|
|
276
|
+
maxWait: 3000,
|
|
277
|
+
})
|
|
278
|
+
p.on("seeked", onSeeked)
|
|
279
|
+
p.on("ratechange", onRatechange)
|
|
280
|
+
p.on("timeupdate", debouncedUpdateResolution)
|
|
281
|
+
p.qualityLevels().on("change", updateResolutionText)
|
|
282
|
+
onRatechange()
|
|
283
|
+
updateResolutionText()
|
|
284
|
+
rateQualityOff = () => {
|
|
285
|
+
p.off("seeked", onSeeked)
|
|
286
|
+
p.off("ratechange", onRatechange)
|
|
287
|
+
p.off("timeupdate", debouncedUpdateResolution)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function openModal(page: string, ev: Event) {
|
|
292
|
+
emit("open-modal", page, ev)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function toggleFullscreen() {
|
|
296
|
+
const p = player.value
|
|
297
|
+
if (!p) return
|
|
298
|
+
if (p.isFullscreen()) {
|
|
299
|
+
p.exitFullscreen()
|
|
300
|
+
} else {
|
|
301
|
+
p.requestFullscreen()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function doPrev() {
|
|
306
|
+
const p = player.value
|
|
307
|
+
if (!p || !props.prevEnabled) return
|
|
308
|
+
if (p.playPrev) {
|
|
309
|
+
p.playPrev()
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
const currentIndex = store.infos.findIndex((info) =>
|
|
313
|
+
isSourceEqual(p.currentSrc(), info),
|
|
314
|
+
)
|
|
315
|
+
if (currentIndex > 0) {
|
|
316
|
+
p.trigger({ type: "openVideo", info: store.infos[currentIndex - 1] })
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function doNext() {
|
|
321
|
+
const p = player.value
|
|
322
|
+
if (!p || !props.nextEnabled) return
|
|
323
|
+
if (p.playNext) {
|
|
324
|
+
p.playNext()
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
const currentIndex = store.infos.findIndex((info) =>
|
|
328
|
+
isSourceEqual(p.currentSrc(), info),
|
|
329
|
+
)
|
|
330
|
+
if (currentIndex >= 0 && currentIndex < store.infos.length - 1) {
|
|
331
|
+
p.trigger({ type: "openVideo", info: store.infos[currentIndex + 1] })
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatTime(sec: number): string {
|
|
336
|
+
if (!Number.isFinite(sec) || sec < 0) return "0:00"
|
|
337
|
+
const totalSeconds = Math.floor(sec)
|
|
338
|
+
const h = Math.floor(totalSeconds / 3600)
|
|
339
|
+
const m = Math.floor((totalSeconds % 3600) / 60)
|
|
340
|
+
const s = totalSeconds % 60
|
|
341
|
+
if (h > 0) {
|
|
342
|
+
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`
|
|
343
|
+
}
|
|
344
|
+
return `${m}:${s.toString().padStart(2, "0")}`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
onMounted(() => {
|
|
348
|
+
const p = player.value
|
|
349
|
+
if (p) subscribeRateQuality(p)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
onUnmounted(() => {
|
|
353
|
+
rateQualityOff?.()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
watch(
|
|
357
|
+
player,
|
|
358
|
+
(p) => {
|
|
359
|
+
rateQualityOff?.()
|
|
360
|
+
if (p) subscribeRateQuality(p)
|
|
361
|
+
},
|
|
362
|
+
{ immediate: false },
|
|
363
|
+
)
|
|
364
|
+
</script>
|
|
365
|
+
|
|
366
|
+
<template>
|
|
367
|
+
<template v-if="player">
|
|
368
|
+
<div
|
|
369
|
+
class="lzc-native-controls relative flex flex-col text-white text-sm font-semibold leading-8"
|
|
370
|
+
>
|
|
371
|
+
<div class="flex flex-row items-center w-full min-h-[3.2rem]">
|
|
372
|
+
<span
|
|
373
|
+
class="text-[12px] font-semibold leading-[20px] text-white mr-[17px] tabular-nums shrink-0"
|
|
374
|
+
>
|
|
375
|
+
{{ formatTime(displayCurrentTime) }}
|
|
376
|
+
</span>
|
|
377
|
+
<div
|
|
378
|
+
ref="progressTrackRef"
|
|
379
|
+
class="flex-1 min-w-0 py-3 flex items-center cursor-pointer -my-3"
|
|
380
|
+
role="slider"
|
|
381
|
+
:aria-valuenow="displayProgressPercent"
|
|
382
|
+
aria-valuemin="0"
|
|
383
|
+
aria-valuemax="100"
|
|
384
|
+
tabindex="0"
|
|
385
|
+
@mousedown="onProgressPointerDown"
|
|
386
|
+
@touchstart="onProgressPointerDown"
|
|
387
|
+
@mouseenter="onProgressMouseEnter"
|
|
388
|
+
@mousemove="onProgressMouseMove"
|
|
389
|
+
@mouseleave="onProgressMouseLeave"
|
|
390
|
+
>
|
|
391
|
+
<div class="flex-1 min-w-0 h-0.5 relative bg-white/30 rounded-sm">
|
|
392
|
+
<div
|
|
393
|
+
v-if="hoverActive && props.duration > 0"
|
|
394
|
+
class="absolute bottom-[calc(100%+8px)] -translate-x-1/2 px-1.5 py-0.5 rounded bg-black/80 text-white text-[11px] leading-4 whitespace-nowrap pointer-events-none"
|
|
395
|
+
:style="{ left: hoverPercent * 100 + '%' }"
|
|
396
|
+
>
|
|
397
|
+
{{ formatTime(hoverTime) }}
|
|
398
|
+
</div>
|
|
399
|
+
<div
|
|
400
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 h-0.5 rounded-sm bg-[#5f86ff] pointer-events-none"
|
|
401
|
+
:style="{ width: displayProgressPercent + '%' }"
|
|
402
|
+
/>
|
|
403
|
+
<div
|
|
404
|
+
class="absolute top-1/2 w-[12px] h-[12px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#5f86ff] shadow-[0_0_0_0.7rem_rgba(95,134,255,0.15)] pointer-events-none"
|
|
405
|
+
:style="{ left: displayProgressPercent + '%' }"
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
<span
|
|
410
|
+
class="text-[12px] font-semibold leading-[20px] text-white ml-[17px] tabular-nums shrink-0"
|
|
411
|
+
>
|
|
412
|
+
{{ formatTime(props.duration) }}
|
|
413
|
+
</span>
|
|
414
|
+
</div>
|
|
415
|
+
<div
|
|
416
|
+
class="flex flex-row items-center justify-between w-full min-h-16 gap-2"
|
|
417
|
+
>
|
|
418
|
+
<div
|
|
419
|
+
class="flex flex-row items-center shrink-0 gap-[12px] sm:gap-[16px]"
|
|
420
|
+
>
|
|
421
|
+
<img
|
|
422
|
+
v-if="!hidePrevButton"
|
|
423
|
+
:src="PrevSvg"
|
|
424
|
+
class="w-[16px] h-[16px] object-contain opacity-100 cursor-pointer flex-shrink-0"
|
|
425
|
+
:class="{ 'opacity-50 cursor-not-allowed': !prevEnabled }"
|
|
426
|
+
@click="doPrev"
|
|
427
|
+
/>
|
|
428
|
+
<img
|
|
429
|
+
:src="props.isPlaying ? PausedSvg : PlaySvg"
|
|
430
|
+
class="w-[16px] h-[16px] object-contain opacity-100 cursor-pointer flex-shrink-0"
|
|
431
|
+
@click="togglePlay"
|
|
432
|
+
/>
|
|
433
|
+
<img
|
|
434
|
+
v-if="!hideNextButton"
|
|
435
|
+
:src="NextSvg"
|
|
436
|
+
class="w-[16px] h-[16px] object-contain opacity-100 cursor-pointer flex-shrink-0"
|
|
437
|
+
:class="{ 'opacity-50 cursor-not-allowed': !nextEnabled }"
|
|
438
|
+
@click="doNext"
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
<div
|
|
442
|
+
class="flex flex-row items-center shrink-0 gap-[12px] sm:gap-[16px] text-white leading-[20px] text-[14px] font-semibold"
|
|
443
|
+
>
|
|
444
|
+
<div
|
|
445
|
+
v-if="!hideSubtitle"
|
|
446
|
+
class="w-auto h-full min-w-[2.4rem] py-0 px-1 border-none bg-transparent cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
|
447
|
+
@click="openModal('Subtitle', $event)"
|
|
448
|
+
>
|
|
449
|
+
{{
|
|
450
|
+
t("src.components.video.components.lzc_sub_button.text", "字幕")
|
|
451
|
+
}}
|
|
452
|
+
</div>
|
|
453
|
+
<div
|
|
454
|
+
v-if="!hidePlayList"
|
|
455
|
+
class="w-auto h-full min-w-[2.4rem] py-0 px-1 border-none bg-transparent cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
|
456
|
+
@click="openModal('LzcPlaylist', $event)"
|
|
457
|
+
>
|
|
458
|
+
{{
|
|
459
|
+
t(
|
|
460
|
+
"src.components.video.components.lzc_playlist_button.text_playlist",
|
|
461
|
+
"播放列表",
|
|
462
|
+
)
|
|
463
|
+
}}
|
|
464
|
+
</div>
|
|
465
|
+
<div
|
|
466
|
+
class="w-auto h-full min-w-[2.4rem] py-0 px-1 border-none bg-transparent cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
|
467
|
+
@click="openModal('LzcPlayrate', $event)"
|
|
468
|
+
>
|
|
469
|
+
{{ playbackRateText }}
|
|
470
|
+
</div>
|
|
471
|
+
<div
|
|
472
|
+
v-if="!hideResolution"
|
|
473
|
+
class="w-auto h-full min-w-[2.4rem] py-0 px-1 border-none bg-transparent cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
|
474
|
+
@click="openModal('LzcResolution', $event)"
|
|
475
|
+
>
|
|
476
|
+
{{ resolutionText }}
|
|
477
|
+
</div>
|
|
478
|
+
<img
|
|
479
|
+
v-if="!isMobile() && fullscreenEnabled"
|
|
480
|
+
:src="props.isFullscreen ? ExitFullscreenSvg : FullscreenSvg"
|
|
481
|
+
class="w-[16px] h-[16px] object-contain opacity-100 cursor-pointer flex-shrink-0"
|
|
482
|
+
@click="toggleFullscreen"
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</template>
|
|
488
|
+
</template>
|
|
489
|
+
|
|
490
|
+
<style scoped>
|
|
491
|
+
.lzc-native-controls {
|
|
492
|
+
padding-right: calc(1.25rem + var(--lzc-player-safe-area-right, 0px));
|
|
493
|
+
padding-bottom: var(--lzc-safe-area-inset-bottom);
|
|
494
|
+
padding-left: calc(1.25rem + var(--lzc-player-safe-area-left, 0px));
|
|
495
|
+
transition: padding 260ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@media (min-width: 640px) {
|
|
499
|
+
.lzc-native-controls {
|
|
500
|
+
padding-right: calc(2rem + var(--lzc-player-safe-area-right, 0px));
|
|
501
|
+
padding-left: calc(2rem + var(--lzc-player-safe-area-left, 0px));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@media (prefers-reduced-motion: reduce) {
|
|
506
|
+
.lzc-native-controls {
|
|
507
|
+
transition: none;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
</style>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed, inject, ref, type CSSProperties } from "vue"
|
|
3
|
+
import { useElementSize } from "@vueuse/core"
|
|
4
|
+
import List from "@/components/Video/components/LzcModal/list.vue"
|
|
5
|
+
import Playrate from "@/components/Video/components/LzcModal/playrate.vue"
|
|
6
|
+
import Resolution from "@/components/Video/components/LzcModal/resolution.vue"
|
|
7
|
+
import Subtitle from "@/components/Video/components/LzcModal/subtitle.vue"
|
|
8
|
+
import type { NativePlayerAPI } from "./NativePlayer"
|
|
9
|
+
import { nativePlayerKey } from "./playerKey"
|
|
10
|
+
|
|
11
|
+
const playerRef = inject(nativePlayerKey, ref<NativePlayerAPI | null>(null))
|
|
12
|
+
const player = computed(() => playerRef.value)
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
visible: boolean
|
|
16
|
+
activePage: string
|
|
17
|
+
position: { right: number; bottom: number }
|
|
18
|
+
directMode?: boolean
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{ (e: "close"): void }>()
|
|
22
|
+
|
|
23
|
+
const modalRootRef = ref<HTMLElement | null>(null)
|
|
24
|
+
const modalContentRef = ref<HTMLElement | null>(null)
|
|
25
|
+
const { width: rootWidth, height: rootHeight } = useElementSize(modalRootRef)
|
|
26
|
+
const { width: contentWidth, height: contentHeight } = useElementSize(modalContentRef)
|
|
27
|
+
|
|
28
|
+
function clamp(value: number, min: number, max: number): number {
|
|
29
|
+
return Math.min(Math.max(value, min), max)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readInheritedPx(name: string): number {
|
|
33
|
+
if (typeof window === "undefined" || typeof document === "undefined") return 0
|
|
34
|
+
const source = modalRootRef.value ?? document.documentElement
|
|
35
|
+
const value = window
|
|
36
|
+
.getComputedStyle(source)
|
|
37
|
+
.getPropertyValue(name)
|
|
38
|
+
const parsed = Number.parseFloat(value)
|
|
39
|
+
if (Number.isFinite(parsed) && value.trim().endsWith("px")) return parsed
|
|
40
|
+
|
|
41
|
+
const probe = document.createElement("div")
|
|
42
|
+
probe.style.position = "absolute"
|
|
43
|
+
probe.style.visibility = "hidden"
|
|
44
|
+
probe.style.pointerEvents = "none"
|
|
45
|
+
probe.style.width = `var(${name})`
|
|
46
|
+
source.appendChild(probe)
|
|
47
|
+
const measured = probe.getBoundingClientRect().width
|
|
48
|
+
probe.remove()
|
|
49
|
+
return Number.isFinite(measured) ? measured : 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function edgeMargin(side: "top" | "right" | "bottom" | "left"): number {
|
|
53
|
+
if (side === "left" || side === "right") {
|
|
54
|
+
return 20 + readInheritedPx(`--lzc-player-safe-area-${side}`)
|
|
55
|
+
}
|
|
56
|
+
return 20 + readInheritedPx(`--lzc-safe-area-inset-${side}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function calcRight(
|
|
60
|
+
targetRight: number,
|
|
61
|
+
elemWidth: number,
|
|
62
|
+
containerWidth: number,
|
|
63
|
+
): number {
|
|
64
|
+
const minRight = edgeMargin("right")
|
|
65
|
+
const maxRight = Math.max(
|
|
66
|
+
minRight,
|
|
67
|
+
containerWidth - elemWidth - edgeMargin("left"),
|
|
68
|
+
)
|
|
69
|
+
return clamp(targetRight - elemWidth / 2, minRight, maxRight)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calcBottom(
|
|
73
|
+
targetBottom: number,
|
|
74
|
+
elemHeight: number,
|
|
75
|
+
containerHeight: number,
|
|
76
|
+
): number {
|
|
77
|
+
const minBottom = edgeMargin("bottom")
|
|
78
|
+
const maxBottom = Math.max(
|
|
79
|
+
minBottom,
|
|
80
|
+
containerHeight - elemHeight - edgeMargin("top"),
|
|
81
|
+
)
|
|
82
|
+
return clamp(targetBottom + 35, minBottom, maxBottom)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const style = computed<CSSProperties>(() => ({
|
|
86
|
+
right: `${calcRight(props.position.right, contentWidth.value, rootWidth.value)}px`,
|
|
87
|
+
bottom: `${calcBottom(props.position.bottom, contentHeight.value, rootHeight.value)}px`,
|
|
88
|
+
position: "absolute",
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
function close() {
|
|
92
|
+
emit("close")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isBlockedInDirectMode = computed(() => {
|
|
96
|
+
if (!props.directMode) return false
|
|
97
|
+
return ["Subtitle", "LzcPlaylist", "LzcResolution"].includes(props.activePage)
|
|
98
|
+
})
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div
|
|
103
|
+
ref="modalRootRef"
|
|
104
|
+
v-show="visible"
|
|
105
|
+
class="absolute inset-0 z-[10] bg-gradient-to-b from-black/80 to-transparent"
|
|
106
|
+
@click.self="close"
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
v-if="!isBlockedInDirectMode"
|
|
110
|
+
ref="modalContentRef"
|
|
111
|
+
class="absolute z-[11] overflow-visible text-white"
|
|
112
|
+
:style="style"
|
|
113
|
+
@click.stop
|
|
114
|
+
>
|
|
115
|
+
<Playrate
|
|
116
|
+
v-if="activePage === 'LzcPlayrate' && player"
|
|
117
|
+
:player="(player as any)"
|
|
118
|
+
/>
|
|
119
|
+
<Resolution
|
|
120
|
+
v-else-if="activePage === 'LzcResolution' && player"
|
|
121
|
+
:player="(player as any)"
|
|
122
|
+
/>
|
|
123
|
+
<List
|
|
124
|
+
v-else-if="activePage === 'LzcPlaylist' && player"
|
|
125
|
+
:player="(player as any)"
|
|
126
|
+
/>
|
|
127
|
+
<Subtitle
|
|
128
|
+
v-else-if="activePage === 'Subtitle' && player"
|
|
129
|
+
:player="(player as any)"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|