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
package/src/main.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createApp } from "vue"
|
|
2
|
+
import App from "./App.vue"
|
|
3
|
+
import router, { formatInfo } from "./router"
|
|
4
|
+
import pinia from "./stores/pinia"
|
|
5
|
+
|
|
6
|
+
// Import Quasar css
|
|
7
|
+
import "quasar/src/css/index.sass"
|
|
8
|
+
import "@/assets/base.scss"
|
|
9
|
+
import "virtual:svg-icons-register"
|
|
10
|
+
import "@lazycatcloud/lzc-file-pickers"
|
|
11
|
+
|
|
12
|
+
import { Quasar, AppFullscreen, Dialog } from "quasar"
|
|
13
|
+
import { useRegisterSW } from "virtual:pwa-register/vue"
|
|
14
|
+
|
|
15
|
+
import i18n, { i18next, I18NextVue } from "./i18n"
|
|
16
|
+
|
|
17
|
+
useRegisterSW({
|
|
18
|
+
immediate: true,
|
|
19
|
+
onRegisteredSW(swUrl: string) {
|
|
20
|
+
console.log(`Service Worker at: ${swUrl}`)
|
|
21
|
+
},
|
|
22
|
+
onRegisterError(error: any) {
|
|
23
|
+
console.error("Service Worker error: ", error)
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// action event
|
|
28
|
+
window.addEventListener("lzc_action_event", function (
|
|
29
|
+
e: CustomEvent<{ type: string; msg: string }>,
|
|
30
|
+
) {
|
|
31
|
+
console.log("action event: ", e)
|
|
32
|
+
switch (e.detail.type) {
|
|
33
|
+
case "OnActivateWindow":
|
|
34
|
+
if (e.detail.msg === window.origin) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
// eslint-disable-next-line no-case-declarations
|
|
38
|
+
const url = new URL(e.detail.msg)
|
|
39
|
+
router.replace({
|
|
40
|
+
name: "link",
|
|
41
|
+
query: formatInfo(
|
|
42
|
+
url.searchParams.get("url"),
|
|
43
|
+
url.searchParams.get("name"),
|
|
44
|
+
),
|
|
45
|
+
})
|
|
46
|
+
break
|
|
47
|
+
default:
|
|
48
|
+
console.log("noimplement event")
|
|
49
|
+
}
|
|
50
|
+
} as EventListener)
|
|
51
|
+
|
|
52
|
+
const app = createApp(App)
|
|
53
|
+
app.use(Quasar, { plugins: { AppFullscreen, Dialog } })
|
|
54
|
+
app.use(pinia)
|
|
55
|
+
app.use(router)
|
|
56
|
+
|
|
57
|
+
router.isReady().then(() => {
|
|
58
|
+
i18n.init().finally(() => {
|
|
59
|
+
app.use(I18NextVue, { i18next })
|
|
60
|
+
app.mount("#app")
|
|
61
|
+
})
|
|
62
|
+
})
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface Doc {
|
|
2
|
+
_id: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface VideoQualityLevel {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
height: number
|
|
9
|
+
width: number
|
|
10
|
+
bitrate: number
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
transport?: "hls" | "direct"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VideoResolution {
|
|
16
|
+
id: string
|
|
17
|
+
res: number
|
|
18
|
+
width: number
|
|
19
|
+
height: number
|
|
20
|
+
auto: boolean
|
|
21
|
+
origin: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PreviewInfo {
|
|
25
|
+
page: number
|
|
26
|
+
image: HTMLImageElement
|
|
27
|
+
len: number
|
|
28
|
+
interval: number
|
|
29
|
+
width: number
|
|
30
|
+
height: number
|
|
31
|
+
tile: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Subtitle {
|
|
35
|
+
name: string
|
|
36
|
+
language: string
|
|
37
|
+
stream_index: number
|
|
38
|
+
codec_name: string
|
|
39
|
+
is_external: boolean
|
|
40
|
+
path: string
|
|
41
|
+
vtt_url?: string
|
|
42
|
+
track_id?: string
|
|
43
|
+
ass_url?: string
|
|
44
|
+
ass_fonts?: string[]
|
|
45
|
+
ass_renderable?: boolean
|
|
46
|
+
ass_unavailable_reason?: string
|
|
47
|
+
/** From subtitle-info API, whether this is the default subtitle track. */
|
|
48
|
+
default?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface VideoInfo extends Doc {
|
|
52
|
+
// Original source URL.
|
|
53
|
+
sourceUrl: string
|
|
54
|
+
name: string
|
|
55
|
+
duration: number
|
|
56
|
+
currentTime: number
|
|
57
|
+
invalid: boolean
|
|
58
|
+
|
|
59
|
+
// Last update time.
|
|
60
|
+
updateTime: number | undefined
|
|
61
|
+
// Whether this video comes from netdisk.
|
|
62
|
+
fromNetdisk: boolean
|
|
63
|
+
// File path in netdisk.
|
|
64
|
+
path: string | undefined
|
|
65
|
+
// subtitle from subtitle-info
|
|
66
|
+
subtitles: Subtitle[] | undefined
|
|
67
|
+
// Remembered subtitle before hidden toggle (Alt+H restore).
|
|
68
|
+
beforeHiddenSubtitle?: Subtitle
|
|
69
|
+
|
|
70
|
+
requestHistory?: boolean
|
|
71
|
+
|
|
72
|
+
playMode?: "direct" | "hls"
|
|
73
|
+
resolvedSourceUrl?: string
|
|
74
|
+
originHlsUrl?: string
|
|
75
|
+
originDirectUrl?: string
|
|
76
|
+
subtitleInfoUrl?: string
|
|
77
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"
|
|
3
|
+
|
|
4
|
+
function basename(url: string | null): string | null {
|
|
5
|
+
if (url == null) {
|
|
6
|
+
return url
|
|
7
|
+
}
|
|
8
|
+
let path = url
|
|
9
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
10
|
+
const u = new URL(url)
|
|
11
|
+
path = u.pathname
|
|
12
|
+
}
|
|
13
|
+
const arr = path.split("/")
|
|
14
|
+
const name = arr[arr.length - 1]
|
|
15
|
+
return name || url
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatInfo(url: string | null, name: string | null) {
|
|
19
|
+
if (url?.startsWith("/") == false) {
|
|
20
|
+
url =
|
|
21
|
+
url.startsWith("http://") || url.startsWith("https://") ? url : "/" + url
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
url,
|
|
25
|
+
name: name ?? basename(url),
|
|
26
|
+
path: url,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mobile: Array<RouteRecordRaw> = [
|
|
31
|
+
{
|
|
32
|
+
path: "/",
|
|
33
|
+
redirect: "/home",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: "/home",
|
|
37
|
+
name: "home",
|
|
38
|
+
component: () => import("@/views/mobile/Home.vue"),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: "/link",
|
|
42
|
+
name: "link",
|
|
43
|
+
component: () => import("@/views/mobile/Player.vue"),
|
|
44
|
+
props: (route) => {
|
|
45
|
+
return route.query
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: "/open",
|
|
50
|
+
name: "open",
|
|
51
|
+
component: () => import("@/views/mobile/Player.vue"),
|
|
52
|
+
props: (route) => {
|
|
53
|
+
let url = typeof route.query.url === "string" ? route.query.url : null
|
|
54
|
+
let name = typeof route.query.name === "string" ? route.query.name : null
|
|
55
|
+
let info = formatInfo(url, name)
|
|
56
|
+
return {
|
|
57
|
+
name: info.name ?? undefined,
|
|
58
|
+
url: info.url ?? undefined,
|
|
59
|
+
path: info.url ?? undefined,
|
|
60
|
+
isUrl:
|
|
61
|
+
typeof route.query.isurl === "string"
|
|
62
|
+
? route.query.isurl == "true"
|
|
63
|
+
: false,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const router = createRouter({
|
|
70
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
71
|
+
routes: mobile,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export default router
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { defineStore } from "pinia"
|
|
2
|
+
import { MiniDB, RemoteDB } from "@lazycatcloud/minidb"
|
|
3
|
+
import { addFrame, delFrame, isFileExist } from "@/use/useVideoFrame"
|
|
4
|
+
import type { VideoInfo } from "@/model"
|
|
5
|
+
import { t } from "@/i18n"
|
|
6
|
+
|
|
7
|
+
const db: MiniDB = new MiniDB()
|
|
8
|
+
|
|
9
|
+
export const useHistoryInfo = () => {
|
|
10
|
+
const innerStore = defineStore("historyInfo", {
|
|
11
|
+
state: () => {
|
|
12
|
+
return {
|
|
13
|
+
collection: {} as RemoteDB<VideoInfo>,
|
|
14
|
+
infos: [] as VideoInfo[],
|
|
15
|
+
ready: Promise.resolve() as Promise<void>,
|
|
16
|
+
/** 从历史记录点击播放时传入的完整 info,供 Player 优先使用 */
|
|
17
|
+
pendingPlayInfo: null as VideoInfo | null,
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
getters: {
|
|
21
|
+
getHistoryInfo(state) {
|
|
22
|
+
return (sourceUrl: string) => {
|
|
23
|
+
return state.infos.find((info) => info.sourceUrl === sourceUrl)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
getHistoryInfoByPath(state) {
|
|
27
|
+
return (path: string) => {
|
|
28
|
+
return state.infos.find((info) => {
|
|
29
|
+
if (info.path && info.path === path) {
|
|
30
|
+
return info
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
actions: {
|
|
37
|
+
// 初始化数据库
|
|
38
|
+
async init() {
|
|
39
|
+
this.ready = (async () => {
|
|
40
|
+
this.collection = db.getCollection("historyInfo")
|
|
41
|
+
await this.refreshData()
|
|
42
|
+
})()
|
|
43
|
+
},
|
|
44
|
+
async refreshData() {
|
|
45
|
+
this.infos = await this.collection
|
|
46
|
+
.find({}, { sort: [["updateTime", "desc"]] })
|
|
47
|
+
.fetch()
|
|
48
|
+
},
|
|
49
|
+
async deleteHistory(info: VideoInfo) {
|
|
50
|
+
await this.collection.remove(info._id)
|
|
51
|
+
await this.refreshData()
|
|
52
|
+
delFrame(info._id)
|
|
53
|
+
return
|
|
54
|
+
},
|
|
55
|
+
async batchDeleteHistory(ids: string[]) {
|
|
56
|
+
if (!ids.length) return
|
|
57
|
+
|
|
58
|
+
await this.collection.remove(ids)
|
|
59
|
+
await this.refreshData()
|
|
60
|
+
|
|
61
|
+
ids.forEach((v) => delFrame(v))
|
|
62
|
+
return
|
|
63
|
+
},
|
|
64
|
+
async updateOrCreateHistory(
|
|
65
|
+
info: VideoInfo,
|
|
66
|
+
blobFn: (() => Promise<Blob | null>) | null,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
if (!info.sourceUrl) {
|
|
69
|
+
throw t("src.stores.playlist.history_error_text", "无效的视频记录")
|
|
70
|
+
}
|
|
71
|
+
const oldInfo = await this.collection.findOne({
|
|
72
|
+
sourceUrl: info["sourceUrl"],
|
|
73
|
+
})
|
|
74
|
+
let newInfo = undefined
|
|
75
|
+
if (oldInfo) {
|
|
76
|
+
newInfo = await this.collection.upsert(
|
|
77
|
+
Object.assign({}, oldInfo, info),
|
|
78
|
+
oldInfo,
|
|
79
|
+
)
|
|
80
|
+
} else {
|
|
81
|
+
info.updateTime = new Date().getTime()
|
|
82
|
+
newInfo = await this.collection.upsert(info)
|
|
83
|
+
}
|
|
84
|
+
if (!newInfo) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const index = this.infos.findIndex(
|
|
88
|
+
(i: VideoInfo) => i.sourceUrl === info.sourceUrl,
|
|
89
|
+
)
|
|
90
|
+
if (index > -1) {
|
|
91
|
+
this.infos.splice(index, 1, newInfo)
|
|
92
|
+
} else {
|
|
93
|
+
this.infos.splice(0, 0, newInfo)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (blobFn && newInfo._id) {
|
|
97
|
+
const id = newInfo._id
|
|
98
|
+
isFileExist(id).then(async (exist) => {
|
|
99
|
+
if (!exist) {
|
|
100
|
+
console.debug("update video frame")
|
|
101
|
+
const blob = await blobFn()
|
|
102
|
+
if (blob) addFrame(id, blob)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async markAsInvalid(sourceUrl: string) {
|
|
108
|
+
const index = this.infos.findIndex(
|
|
109
|
+
(i: VideoInfo) => i.sourceUrl === sourceUrl,
|
|
110
|
+
)
|
|
111
|
+
if (index < 0) {
|
|
112
|
+
throw t(
|
|
113
|
+
"src.stores.playlist.not_exist_error_text",
|
|
114
|
+
"{{sourceUrl}} 不存在",
|
|
115
|
+
{ sourceUrl },
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
const info = this.infos[index]
|
|
119
|
+
const newInfo = await this.collection.upsert(
|
|
120
|
+
Object.assign({}, info, { invalid: true }),
|
|
121
|
+
info,
|
|
122
|
+
)
|
|
123
|
+
this.infos.splice(index, 1, newInfo!)
|
|
124
|
+
},
|
|
125
|
+
setPendingPlayInfo(info: VideoInfo | null) {
|
|
126
|
+
this.pendingPlayInfo = info
|
|
127
|
+
},
|
|
128
|
+
async clear(): Promise<void> {
|
|
129
|
+
const ids: string[] = []
|
|
130
|
+
this.infos.forEach((info: VideoInfo) => {
|
|
131
|
+
if (info._id) {
|
|
132
|
+
ids.push(info._id)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
await this.collection.remove(ids)
|
|
136
|
+
this.infos = []
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const s = innerStore()
|
|
142
|
+
if (!s.collection.upsert) {
|
|
143
|
+
s.init()
|
|
144
|
+
}
|
|
145
|
+
return s
|
|
146
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ref } from "vue"
|
|
2
|
+
import { useEventListener } from "@vueuse/core"
|
|
3
|
+
|
|
4
|
+
type KeyCallbackMap = Record<string, (event: KeyboardEvent) => void>
|
|
5
|
+
|
|
6
|
+
export function useKeyBind(map: KeyCallbackMap, shortcutToggle = ref(true)) {
|
|
7
|
+
let isHolded = false
|
|
8
|
+
let holdTimeout: ReturnType<typeof setTimeout> | null = null
|
|
9
|
+
const modifierBlockedCodes = new Set<string>()
|
|
10
|
+
|
|
11
|
+
useEventListener("keydown", (e) => {
|
|
12
|
+
if (!shortcutToggle.value) return
|
|
13
|
+
if (!isBind(e.code)) return
|
|
14
|
+
if (hasModifierKey(e)) {
|
|
15
|
+
modifierBlockedCodes.add(e.code)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if (isHolded) return
|
|
19
|
+
isHolded = true
|
|
20
|
+
holdTimeout = setTimeout(() => {
|
|
21
|
+
const cb = () => {
|
|
22
|
+
exec(e)
|
|
23
|
+
if (isHolded) {
|
|
24
|
+
holdTimeout = setTimeout(cb, 100)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
cb()
|
|
28
|
+
}, 500)
|
|
29
|
+
})
|
|
30
|
+
useEventListener("keyup", (e) => {
|
|
31
|
+
const blockedByModifier = modifierBlockedCodes.has(e.code)
|
|
32
|
+
modifierBlockedCodes.delete(e.code)
|
|
33
|
+
reset()
|
|
34
|
+
if (!shortcutToggle.value) return
|
|
35
|
+
if (!isBind(e.code)) return
|
|
36
|
+
if (blockedByModifier || hasModifierKey(e)) return
|
|
37
|
+
exec(e)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function reset() {
|
|
41
|
+
if (holdTimeout) {
|
|
42
|
+
clearTimeout(holdTimeout)
|
|
43
|
+
holdTimeout = null
|
|
44
|
+
}
|
|
45
|
+
isHolded = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasModifierKey(e: KeyboardEvent) {
|
|
49
|
+
return e.shiftKey || e.ctrlKey || e.altKey || e.metaKey
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isBind(code: string) {
|
|
53
|
+
return code in map
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function exec(e: KeyboardEvent) {
|
|
57
|
+
const keyCode = e.code
|
|
58
|
+
const fn = map[keyCode]
|
|
59
|
+
fn && fn?.(e)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { computed, ref } from "vue"
|
|
2
|
+
|
|
3
|
+
export default function () {
|
|
4
|
+
// 是否编辑中
|
|
5
|
+
const isEditing = ref(false)
|
|
6
|
+
// 已选择的item 主键列表
|
|
7
|
+
const selectedKeys = ref<string[]>([])
|
|
8
|
+
// 数据
|
|
9
|
+
const data = ref<any[]>([])
|
|
10
|
+
// 是否全选了
|
|
11
|
+
const isSelectAll = computed(
|
|
12
|
+
() =>
|
|
13
|
+
selectedKeys.value.length > 0 &&
|
|
14
|
+
data.value.length === selectedKeys.value.length,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// 全选 / 取消全选
|
|
18
|
+
function hdlSelectAll() {
|
|
19
|
+
if (isSelectAll.value) {
|
|
20
|
+
selectedKeys.value = []
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
selectedKeys.value = data.value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 取消编辑,初始化参数
|
|
27
|
+
function hdlCancel() {
|
|
28
|
+
isEditing.value = false
|
|
29
|
+
selectedKeys.value = []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 切换item是否选中
|
|
33
|
+
function hdlToggleItem(item: any) {
|
|
34
|
+
// 如果已经选中则取消选中
|
|
35
|
+
if (selectedKeys.value.includes(item)) {
|
|
36
|
+
selectedKeys.value = selectedKeys.value.filter((v) => v !== item)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
selectedKeys.value.push(item)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 刷新数据
|
|
44
|
+
function updateData(values: any[]) {
|
|
45
|
+
console.log("updateData")
|
|
46
|
+
data.value = values
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
selectedKeys,
|
|
51
|
+
|
|
52
|
+
isEditing,
|
|
53
|
+
isSelectAll,
|
|
54
|
+
|
|
55
|
+
hdlSelectAll,
|
|
56
|
+
hdlCancel,
|
|
57
|
+
hdlToggleItem,
|
|
58
|
+
updateData,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MiniDB, RemoteDB } from "@lazycatcloud/minidb"
|
|
2
|
+
|
|
3
|
+
const db = new MiniDB()
|
|
4
|
+
const GLOBAL_LAST_SUBTITLE_KEY = "__global_last_subtitle__"
|
|
5
|
+
|
|
6
|
+
type SubtitleStateRecord = {
|
|
7
|
+
path: string
|
|
8
|
+
name?: string
|
|
9
|
+
index?: number
|
|
10
|
+
is_external?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const state: RemoteDB<SubtitleStateRecord> = db.getCollection("subtitle")
|
|
14
|
+
|
|
15
|
+
function update(path: string, value: { name?: string; index?: number }) {
|
|
16
|
+
state.upsertOrUpdate({ path }, { path, ...value })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findOne(path: string) {
|
|
20
|
+
return state.findOne({ path })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function updateLastSelected(value: { name?: string; is_external?: boolean }) {
|
|
24
|
+
state.upsertOrUpdate(
|
|
25
|
+
{ path: GLOBAL_LAST_SUBTITLE_KEY },
|
|
26
|
+
{ path: GLOBAL_LAST_SUBTITLE_KEY, ...value },
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findLastSelected() {
|
|
31
|
+
return state.findOne({ path: GLOBAL_LAST_SUBTITLE_KEY })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const subtitleDB = {
|
|
35
|
+
update,
|
|
36
|
+
findOne,
|
|
37
|
+
updateLastSelected,
|
|
38
|
+
findLastSelected,
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { VideoInfo } from "@/model";
|
|
2
|
+
|
|
3
|
+
export function isMobile() {
|
|
4
|
+
return (
|
|
5
|
+
navigator.userAgent.search(
|
|
6
|
+
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i,
|
|
7
|
+
) > -1
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 带字幕会在url上加sub_type、custom_sub、subtitle
|
|
12
|
+
export function isSourceEqual(src: string, info: VideoInfo) {
|
|
13
|
+
const normalize = (value: string) => {
|
|
14
|
+
const u = new URL(value)
|
|
15
|
+
u.searchParams.delete("sub_type")
|
|
16
|
+
u.searchParams.delete("custom_sub")
|
|
17
|
+
u.searchParams.delete("subtitle")
|
|
18
|
+
u.pathname = u.pathname.replace(/\/quality-[^/]*\.m3u8$/i, "/master.m3u8")
|
|
19
|
+
return u.toString()
|
|
20
|
+
}
|
|
21
|
+
return normalize(src) === normalize(info.sourceUrl)
|
|
22
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 实现播放列表中的封面和拖动进度条时展示的小窗口预览
|
|
2
|
+
// 图片使用 lzcinit 中提供的 webdav 服务进行储存到 /app/var 目录下
|
|
3
|
+
// 图片使用播放列表中 minidb id 进行命名(即 webdav 中使用文件名和播放列表中进行关联)
|
|
4
|
+
|
|
5
|
+
import { createClient } from "webdav/web"
|
|
6
|
+
import EmptySvg from "@/icons/暂无.svg?inline"
|
|
7
|
+
import BackgroundSvg from "@/icons/移动端_背景.webp?inline"
|
|
8
|
+
|
|
9
|
+
const client = createClient("/_lzc/files/app/user/")
|
|
10
|
+
let putLock: boolean = false
|
|
11
|
+
|
|
12
|
+
export async function isFileExist(name: string) {
|
|
13
|
+
return client.exists(name)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getFileLinkWithDefault(name: string) {
|
|
17
|
+
if (name == "") {
|
|
18
|
+
return BackgroundSvg
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const bool = await client.exists(name)
|
|
22
|
+
if (bool) {
|
|
23
|
+
return client.getFileDownloadLink(name)
|
|
24
|
+
} else {
|
|
25
|
+
return BackgroundSvg
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 获取图片的下载连接
|
|
30
|
+
export const getFrame = (id: string) => {
|
|
31
|
+
if (id) {
|
|
32
|
+
return client.getFileDownloadLink(id)
|
|
33
|
+
} else {
|
|
34
|
+
return EmptySvg
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 上传图片
|
|
39
|
+
export const addFrame = async (id: string, blob: Blob): Promise<boolean> => {
|
|
40
|
+
if (putLock) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
putLock = true
|
|
46
|
+
const buffer = await blob.arrayBuffer()
|
|
47
|
+
const ok = await client.putFileContents(id, buffer, {
|
|
48
|
+
contentLength: false,
|
|
49
|
+
overwrite: true,
|
|
50
|
+
})
|
|
51
|
+
return ok
|
|
52
|
+
} finally {
|
|
53
|
+
putLock = false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 删除图片
|
|
58
|
+
export const delFrame = async (id: string) => {
|
|
59
|
+
return client.deleteFile(id)
|
|
60
|
+
}
|