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.
Files changed (162) hide show
  1. package/.dockerignore +1 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.prettierrc.json +5 -0
  4. package/AGENTS.md +31 -0
  5. package/README.md +38 -0
  6. package/build.sh +10 -0
  7. package/demo/.vscode/extensions.json +3 -0
  8. package/demo/README.md +40 -0
  9. package/demo/env.d.ts +1 -0
  10. package/demo/index.html +13 -0
  11. package/demo/package-lock.json +2037 -0
  12. package/demo/package.json +25 -0
  13. package/demo/public/favicon.ico +0 -0
  14. package/demo/src/App.vue +25 -0
  15. package/demo/src/assets/base.css +70 -0
  16. package/demo/src/assets/logo.svg +1 -0
  17. package/demo/src/assets/main.css +33 -0
  18. package/demo/src/main.ts +8 -0
  19. package/demo/tsconfig.config.json +8 -0
  20. package/demo/tsconfig.json +16 -0
  21. package/demo/vite.config.ts +14 -0
  22. package/docs/progress-bar-style-analysis.md +87 -0
  23. package/env.d.ts +1 -0
  24. package/error_pages/502.html.tpl +13 -0
  25. package/i18next-parser.config.mjs +147 -0
  26. package/index.html +54 -0
  27. package/lazycat.png +0 -0
  28. package/lib/README.md +48 -0
  29. package/lib/package.json +22 -0
  30. package/lzc-build.local.yml +65 -0
  31. package/lzc-build.yml +65 -0
  32. package/lzc-manifest.yml +53 -0
  33. package/makefile +15 -0
  34. package/package.json +69 -0
  35. package/postcss.config.js +6 -0
  36. package/public/512x512.png +0 -0
  37. package/public/favicon.ico +0 -0
  38. package/public/languages/en/translation.json +125 -0
  39. package/public/languages/zh/translation.json +125 -0
  40. package/public/libass-wasm/4.1.0/default.woff2 +0 -0
  41. package/public/libass-wasm/4.1.0/subtitles-octopus-worker-legacy.js +40 -0
  42. package/public/libass-wasm/4.1.0/subtitles-octopus-worker.js +1 -0
  43. package/public/libass-wasm/4.1.0/subtitles-octopus-worker.wasm +0 -0
  44. package/public/libass-wasm/4.1.0/subtitles-octopus.js +1680 -0
  45. package/public/square-128x128.png +0 -0
  46. package/public/square-256x256.png +0 -0
  47. package/public/square-512x512.png +0 -0
  48. package/src/App.vue +18 -0
  49. package/src/assets/base.scss +104 -0
  50. package/src/assets/cloud.png +0 -0
  51. package/src/assets/logo.svg +1 -0
  52. package/src/components/Dialog/index.vue +96 -0
  53. package/src/components/MultipleEdit/choose.vue +39 -0
  54. package/src/components/PlayList/index.vue +521 -0
  55. package/src/components/Spectrum/index.vue +58 -0
  56. package/src/components/Video/NativeVideoPlayer.vue +748 -0
  57. package/src/components/Video/README.md +3 -0
  58. package/src/components/Video/clientPlayer.ts +348 -0
  59. package/src/components/Video/components/LzcModal/components/simpleList.vue +57 -0
  60. package/src/components/Video/components/LzcModal/list.vue +52 -0
  61. package/src/components/Video/components/LzcModal/playrate.vue +45 -0
  62. package/src/components/Video/components/LzcModal/resolution.vue +117 -0
  63. package/src/components/Video/components/LzcModal/subtitle.vue +499 -0
  64. package/src/components/Video/components/LzcModal/useModal.ts +18 -0
  65. package/src/components/Video/components/LzcOverlay/SubtitleLayer.vue +321 -0
  66. package/src/components/Video/components/LzcOverlay/cast.vue +253 -0
  67. package/src/components/Video/components/LzcOverlay/casting.vue +205 -0
  68. package/src/components/Video/components/LzcOverlay/error.vue +103 -0
  69. package/src/components/Video/components/LzcOverlay/helper.ts +81 -0
  70. package/src/components/Video/components/LzcOverlay/index.vue +99 -0
  71. package/src/components/Video/components/LzcOverlay/playing.vue +496 -0
  72. package/src/components/Video/components/LzcOverlay/playingButtons.vue +122 -0
  73. package/src/components/Video/components/LzcOverlay/playingLayout.vue +287 -0
  74. package/src/components/Video/components/LzcOverlay/useCast.ts +235 -0
  75. package/src/components/Video/components/LzcOverlay/useCommon.ts +41 -0
  76. package/src/components/Video/components/LzcOverlay/useOctopusRenderer.ts +230 -0
  77. package/src/components/Video/components/LzcOverlay/useSubtitleRenderEngine.ts +79 -0
  78. package/src/components/Video/components/LzcOverlay/useSubtitleTrack.ts +139 -0
  79. package/src/components/Video/components/useLzcCommon.ts +16 -0
  80. package/src/components/Video/directPlay.ts +345 -0
  81. package/src/components/Video/getSubtitleInfo.ts +42 -0
  82. package/src/components/Video/native/EventEmitter.ts +62 -0
  83. package/src/components/Video/native/NativeControls.vue +510 -0
  84. package/src/components/Video/native/NativeModal.vue +133 -0
  85. package/src/components/Video/native/NativePlayer.ts +913 -0
  86. package/src/components/Video/native/NativePlayer.vue +53 -0
  87. package/src/components/Video/native/index.ts +9 -0
  88. package/src/components/Video/native/native-player.css +183 -0
  89. package/src/components/Video/native/playerKey.ts +5 -0
  90. package/src/components/Video/native/useNativeCastMiddleware.ts +50 -0
  91. package/src/components/Video/native/useNativePlayer.ts +3 -0
  92. package/src/components/Video/native/useNativePlayerFullscreen.ts +44 -0
  93. package/src/components/Video/native/useNativePlayerHistory.ts +69 -0
  94. package/src/components/Video/native/useNativePlayerModal.ts +68 -0
  95. package/src/components/Video/native/useNativePlayerPlaylist.ts +67 -0
  96. package/src/components/Video/native/useNativePlayerState.ts +225 -0
  97. package/src/components/Video/player.ts +99 -0
  98. package/src/components/Video/theme/index.scss +291 -0
  99. package/src/components/Video/theme/videojs.css +1797 -0
  100. package/src/components/Video/useSource.ts +1431 -0
  101. package/src/components/Video/useSubtitlePreference.ts +66 -0
  102. package/src/components/Video/useWebview.ts +79 -0
  103. package/src/components/Video/videoFrame.ts +58 -0
  104. package/src/env.d.ts +3 -0
  105. package/src/i18n/README.md +392 -0
  106. package/src/i18n/index.ts +49 -0
  107. package/src/icons/Video_Player.svg +69 -0
  108. package/src/icons/box.svg +15 -0
  109. package/src/icons/client.svg +17 -0
  110. package/src/icons/logo.svg +28 -0
  111. package/src/icons//344/270/212/344/270/200/344/270/252.svg +6 -0
  112. package/src/icons//344/270/213/344/270/200/344/270/252.svg +4 -0
  113. package/src/icons//344/272/256/345/272/246.svg +13 -0
  114. package/src/icons//345/200/215/351/200/237.svg +14 -0
  115. package/src/icons//345/205/250/345/261/217.svg +16 -0
  116. package/src/icons//345/205/250/351/200/211_/345/267/262/351/200/211/344/270/255.svg +16 -0
  117. package/src/icons//345/205/250/351/200/211_/346/234/252/351/200/211/344/270/255.svg +15 -0
  118. package/src/icons//345/205/263/351/227/255/345/244/232/351/200/211.svg +14 -0
  119. package/src/icons//345/205/263/351/227/255/346/212/225/345/261/217.svg +11 -0
  120. package/src/icons//345/233/236/346/224/266/347/253/231.svg +15 -0
  121. package/src/icons//345/244/261/346/225/210.svg +17 -0
  122. package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250-icon.png +0 -0
  123. package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250.png +0 -0
  124. package/src/icons//346/212/225/345/261/217.svg +11 -0
  125. package/src/icons//346/212/225/351/200/201/344/270/255.jpg +0 -0
  126. package/src/icons//346/212/225/351/200/201/344/270/255.svg +21 -0
  127. package/src/icons//346/222/255/346/224/276.svg +3 -0
  128. package/src/icons//346/232/202/345/201/234.svg +4 -0
  129. package/src/icons//346/232/202/346/227/240.svg +21 -0
  130. package/src/icons//346/233/264/345/244/232/346/223/215/344/275/234.svg +11 -0
  131. package/src/icons//347/224/265/350/247/206.svg +18 -0
  132. package/src/icons//347/247/273/345/212/250/347/253/257_/350/203/214/346/231/257.webp +0 -0
  133. package/src/icons//350/203/214/346/231/257.png +0 -0
  134. package/src/icons//350/277/224/345/233/236.svg +13 -0
  135. package/src/icons//350/277/233/345/205/245/345/205/250/345/261/217.svg +13 -0
  136. package/src/icons//351/200/200/345/207/272/345/205/250/345/261/217.svg +15 -0
  137. package/src/icons//351/200/211/346/213/251.svg +15 -0
  138. package/src/icons//351/237/263/351/207/217.svg +13 -0
  139. package/src/index.d.ts +9 -0
  140. package/src/lzc-video-player.scss +7 -0
  141. package/src/lzc-video-player.ts +6 -0
  142. package/src/main.ts +62 -0
  143. package/src/model.ts +77 -0
  144. package/src/quasar-variables.sass +10 -0
  145. package/src/router/index.ts +74 -0
  146. package/src/stores/pinia.ts +3 -0
  147. package/src/stores/playlist.ts +146 -0
  148. package/src/use/useKeyBind.ts +61 -0
  149. package/src/use/useMultipleEdit.ts +60 -0
  150. package/src/use/useSdk.ts +5 -0
  151. package/src/use/useSubtitle.ts +39 -0
  152. package/src/use/useUtils.ts +22 -0
  153. package/src/use/useVideoFrame.ts +60 -0
  154. package/src/views/Home.ts +99 -0
  155. package/src/views/mobile/Home.vue +246 -0
  156. package/src/views/mobile/Player.vue +141 -0
  157. package/tailwind.config.js +15 -0
  158. package/tsconfig.config.json +8 -0
  159. package/tsconfig.json +20 -0
  160. package/vite.config.lib.ts +88 -0
  161. package/vite.config.ts +122 -0
  162. 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,10 @@
1
+ $primary : #1976D2
2
+ $secondary : #26A69A
3
+ $accent : #9C27B0
4
+
5
+ $dark : #1D1D1D
6
+
7
+ $positive : #21BA45
8
+ $negative : #C10015
9
+ $info : #31CCEC
10
+ $warning : #F2C037
@@ -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,3 @@
1
+ import { createPinia, getActivePinia } from "pinia"
2
+ const pinia = getActivePinia() ?? createPinia()
3
+ export default pinia
@@ -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,5 @@
1
+ import { lzcAPIGateway } from "@lazycatcloud/sdk"
2
+ import LzcApp from "@lazycatcloud/sdk/dist/extentions/base"
3
+ const lzcApi = new lzcAPIGateway("/_lzc/runtime/grpc/")
4
+ export default lzcApi
5
+ export { LzcApp }
@@ -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
+ }