feat: ui改版
This commit is contained in:
parent
c0428c5d3d
commit
7edb8e4d93
993
ui/package-lock.json
generated
993
ui/package-lock.json
generated
@ -1,993 +0,0 @@
|
||||
{
|
||||
"name": "iptv-web-core",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "iptv-web-core",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"hls.js": "^1.5.0",
|
||||
"idb": "^8.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.27",
|
||||
"entities": "^7.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.9",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/runtime-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.2.0.tgz",
|
||||
"integrity": "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.2.0",
|
||||
"@vueuse/shared": "14.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.2.0.tgz",
|
||||
"integrity": "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.2.0.tgz",
|
||||
"integrity": "sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
|
||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"dependencies": {
|
||||
"is-what": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz",
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.9"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-sfc": "3.5.27",
|
||||
"@vue/runtime-dom": "3.5.27",
|
||||
"@vue/server-renderer": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.27.1"
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5"
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"requires": {
|
||||
"@babel/types": "^7.29.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
}
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5"
|
||||
},
|
||||
"@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"dev": true
|
||||
},
|
||||
"@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||
},
|
||||
"@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.27",
|
||||
"entities": "^7.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"@vue/devtools-api": {
|
||||
"version": "6.6.4"
|
||||
},
|
||||
"@vue/devtools-kit": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||
"requires": {
|
||||
"@vue/devtools-shared": "^7.7.9",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"@vue/devtools-shared": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||
"requires": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/runtime-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.5.27"
|
||||
},
|
||||
"@vueuse/core": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.2.0.tgz",
|
||||
"integrity": "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==",
|
||||
"requires": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.2.0",
|
||||
"@vueuse/shared": "14.2.0"
|
||||
}
|
||||
},
|
||||
"@vueuse/metadata": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.2.0.tgz",
|
||||
"integrity": "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ=="
|
||||
},
|
||||
"@vueuse/shared": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.2.0.tgz",
|
||||
"integrity": "sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA==",
|
||||
"requires": {}
|
||||
},
|
||||
"birpc": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
|
||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="
|
||||
},
|
||||
"copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"requires": {
|
||||
"is-what": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.2.3"
|
||||
},
|
||||
"entities": {
|
||||
"version": "7.0.1"
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.21.5",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"estree-walker": {
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.3",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"hls.js": {
|
||||
"version": "1.6.15"
|
||||
},
|
||||
"hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
|
||||
},
|
||||
"idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz",
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.30.21",
|
||||
"requires": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.11"
|
||||
},
|
||||
"perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||
"requires": {
|
||||
"@vue/devtools-kit": "^7.7.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.5.6",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
|
||||
},
|
||||
"rollup": {
|
||||
"version": "4.57.1",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"@types/estree": "1.0.8",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.2.1"
|
||||
},
|
||||
"speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
|
||||
},
|
||||
"superjson": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"requires": {
|
||||
"copy-anything": "^4"
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "5.4.21",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.21.3",
|
||||
"fsevents": "~2.3.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.5.27",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-sfc": "3.5.27",
|
||||
"@vue/runtime-dom": "3.5.27",
|
||||
"@vue/server-renderer": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "4.6.4",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,9 +15,7 @@
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"hls.js": "^1.5.0",
|
||||
"idb": "^8.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
|
||||
60
ui/pnpm-lock.yaml
generated
60
ui/pnpm-lock.yaml
generated
@ -8,15 +8,18 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@vueuse/core':
|
||||
specifier: ^14.2.0
|
||||
version: 14.2.1(vue@3.5.27)
|
||||
hls.js:
|
||||
specifier: ^1.5.0
|
||||
version: 1.6.15
|
||||
idb:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.5.27
|
||||
vue-router:
|
||||
specifier: ^4.2.0
|
||||
version: 4.6.4(vue@3.5.27)
|
||||
devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.0.0
|
||||
@ -326,6 +329,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4':
|
||||
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@ -345,9 +351,6 @@ packages:
|
||||
'@vue/compiler-ssr@3.5.27':
|
||||
resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==}
|
||||
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/reactivity@3.5.27':
|
||||
resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==}
|
||||
|
||||
@ -365,6 +368,19 @@ packages:
|
||||
'@vue/shared@3.5.27':
|
||||
resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==}
|
||||
|
||||
'@vueuse/core@14.2.1':
|
||||
resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/metadata@14.2.1':
|
||||
resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
|
||||
|
||||
'@vueuse/shared@14.2.1':
|
||||
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@ -388,6 +404,9 @@ packages:
|
||||
hls.js@1.6.15:
|
||||
resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==}
|
||||
|
||||
idb@8.0.3:
|
||||
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@ -443,11 +462,6 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vue-router@4.6.4:
|
||||
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
vue@3.5.27:
|
||||
resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==}
|
||||
peerDependencies:
|
||||
@ -619,6 +633,8 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.27)':
|
||||
dependencies:
|
||||
vite: 5.4.21
|
||||
@ -654,8 +670,6 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.27
|
||||
'@vue/shared': 3.5.27
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/reactivity@3.5.27':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.27
|
||||
@ -680,6 +694,19 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.27': {}
|
||||
|
||||
'@vueuse/core@14.2.1(vue@3.5.27)':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 14.2.1
|
||||
'@vueuse/shared': 14.2.1(vue@3.5.27)
|
||||
vue: 3.5.27
|
||||
|
||||
'@vueuse/metadata@14.2.1': {}
|
||||
|
||||
'@vueuse/shared@14.2.1(vue@3.5.27)':
|
||||
dependencies:
|
||||
vue: 3.5.27
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
@ -717,6 +744,8 @@ snapshots:
|
||||
|
||||
hls.js@1.6.15: {}
|
||||
|
||||
idb@8.0.3: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@ -772,11 +801,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
vue-router@4.6.4(vue@3.5.27):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.27
|
||||
|
||||
vue@3.5.27:
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.27
|
||||
|
||||
287
ui/src/App.vue
287
ui/src/App.vue
@ -1,284 +1,39 @@
|
||||
<template>
|
||||
<div class="iptv-app">
|
||||
<!-- 全屏视频播放器 -->
|
||||
<VideoPlayer ref="playerRef" :url="currentUrl" @error="handlePlayError" />
|
||||
<!-- <VideoPlayer /> -->
|
||||
|
||||
<!-- 左侧面板(四栏) -->
|
||||
<LeftPanel
|
||||
:current-channel="currentChannel"
|
||||
:validity-map="validityMap"
|
||||
@play="handlePlay"
|
||||
/>
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<BottomPanel
|
||||
:channel="currentChannel"
|
||||
:current-source-index="currentSourceIndex"
|
||||
:current-program="currentProgramTitle"
|
||||
:progress="programProgress"
|
||||
:current-time="currentTime"
|
||||
:total-time="totalTime"
|
||||
:is-favorite="isFavorite(currentChannel?.id)"
|
||||
@favorite="toggleFavorite(currentChannel?.id)"
|
||||
@switch-source="showSourceSelector = true"
|
||||
@settings="showSettings = true"
|
||||
/>
|
||||
|
||||
<!-- 线路选择弹窗 -->
|
||||
<SourceModal
|
||||
v-if="showSourceSelector"
|
||||
:channel="currentChannel"
|
||||
:current-url="currentUrl"
|
||||
:validity-map="validityMap"
|
||||
@switch="switchSource"
|
||||
@close="showSourceSelector = false"
|
||||
/>
|
||||
|
||||
<!-- 设置弹窗 -->
|
||||
<SettingsModal
|
||||
v-if="showSettings"
|
||||
@close="showSettings = false"
|
||||
@reload="reloadChannels"
|
||||
/>
|
||||
<!-- <LeftPanel /> -->
|
||||
|
||||
<!-- 遥控数字输入显示 -->
|
||||
<InputPanel @complete="handleRemoteInputComplete" />
|
||||
<!-- <InputPanel /> -->
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<!-- <BottomPanel /> -->
|
||||
|
||||
<!-- 线路选择弹窗 -->
|
||||
<!-- <SourceModal v-if="showSourceSelector" /> -->
|
||||
|
||||
<!-- 设置弹窗 -->
|
||||
<!-- <SettingsModal v-if="showSettings" /> -->
|
||||
|
||||
<!-- 调试面板 -->
|
||||
<DebugPanel />
|
||||
<!-- <DebugPanel /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useStorage } from "./composables/useStorage.js";
|
||||
import { useUI } from "./composables/useUI.js";
|
||||
import { useFavorites } from "./composables/useFavorites.js";
|
||||
import { useHistory } from "./composables/useHistory.js";
|
||||
import { useChannels } from "./composables/useChannels.js";
|
||||
import DebugPanel from "./components/Layout/DebugPanel.vue";
|
||||
import VideoPlayer from "./components/VideoPlayer.vue";
|
||||
import LeftPanel from "./components/Layout/LeftPanel.vue";
|
||||
import InputPanel from "./components/Layout/InputPanel.vue";
|
||||
import BottomPanel from "./components/Layout/BottomPanel.vue";
|
||||
import SourceModal from "./components/Modals/SourceModal.vue";
|
||||
import SettingsModal from "./components/Modals/SettingsModal.vue";
|
||||
import { useKeyEvent } from "./composables/useEvent.js";
|
||||
// import VideoPlayer from "./components/VideoPlayer.vue";
|
||||
// import LeftPanel from "./components/Layout/LeftPanel.vue";
|
||||
// import InputPanel from "./components/Layout/InputPanel.vue";
|
||||
// import BottomPanel from "./components/Layout/BottomPanel.vue";
|
||||
// import SourceModal from "./components/Modals/SourceModal.vue";
|
||||
// import SettingsModal from "./components/Modals/SettingsModal.vue";
|
||||
// import DebugPanel from "./components/Layout/DebugPanel.vue";
|
||||
import { useStorage } from "./composables/useStorage";
|
||||
|
||||
// Storage(底层存储接口,只负责数据存取)
|
||||
const storage = useStorage();
|
||||
|
||||
// UI 状态
|
||||
const { showLeftPanel, showBottomPanel, hideLeftPanel } = useUI();
|
||||
|
||||
// 业务逻辑 hooks(内部使用 useStorage 持久化)
|
||||
const { toggleFavorite, isFavorite } = useFavorites();
|
||||
const { addToHistory } = useHistory();
|
||||
const { channels, loadChannels } = useChannels();
|
||||
|
||||
const showSourceSelector = ref(false);
|
||||
const showSettings = ref(false);
|
||||
|
||||
// 播放器
|
||||
const playerRef = ref(null);
|
||||
const currentUrl = ref("");
|
||||
const currentSourceIndex = ref(0);
|
||||
|
||||
// 播放状态
|
||||
const currentChannel = ref(null);
|
||||
|
||||
// 播放频道
|
||||
async function handlePlay(channel) {
|
||||
if (!channel || !channel.urls || channel.urls.length === 0) return;
|
||||
|
||||
currentChannel.value = channel;
|
||||
|
||||
// 测速排序后选择最佳线路
|
||||
// const sortedUrls = await sortUrlsBySpeed(channel.urls);
|
||||
// currentUrl.value = sortedUrls[0] || channel.urls[0];
|
||||
currentUrl.value = channel.urls[0];
|
||||
currentSourceIndex.value = 0;
|
||||
|
||||
showBottomPanel();
|
||||
hideLeftPanel();
|
||||
|
||||
// 添加到播放历史
|
||||
await addToHistory(channel);
|
||||
}
|
||||
|
||||
// 测速排序
|
||||
async function sortUrlsBySpeed(urls) {
|
||||
if (!urls || urls.length === 0) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
// 检查缓存
|
||||
const cached = validityMap.value.get(url);
|
||||
if (cached && Date.now() - cached.checkedAt < 60 * 60 * 1000) {
|
||||
// 1小时内缓存有效
|
||||
return { url, ...cached };
|
||||
}
|
||||
|
||||
// HTTP HEAD 测速
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "HEAD",
|
||||
signal: controller.signal,
|
||||
mode: "no-cors", // 允许跨域
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
const validity = {
|
||||
status: "online",
|
||||
checkedAt: Date.now(),
|
||||
latency,
|
||||
failCount: 0,
|
||||
};
|
||||
|
||||
validityMap.value.set(url, validity);
|
||||
await storage.setValidity(url, validity);
|
||||
|
||||
return { url, ...validity };
|
||||
} catch (error) {
|
||||
// 测速失败
|
||||
const cached = validityMap.value.get(url);
|
||||
const failCount = (cached?.failCount || 0) + 1;
|
||||
|
||||
const validity = {
|
||||
status: failCount >= 3 ? "offline" : "unknown",
|
||||
checkedAt: Date.now(),
|
||||
latency: Infinity,
|
||||
failCount,
|
||||
};
|
||||
|
||||
validityMap.value.set(url, validity);
|
||||
await storage.setValidity(url, validity);
|
||||
|
||||
return { url, ...validity };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 排序:在线的按延迟排序,离线的放最后
|
||||
results.sort((a, b) => {
|
||||
if (a.status === "offline" && b.status !== "offline") return 1;
|
||||
if (a.status !== "offline" && b.status === "offline") return -1;
|
||||
return a.latency - b.latency;
|
||||
});
|
||||
|
||||
return results.map((r) => r.url);
|
||||
}
|
||||
|
||||
// 切换线路
|
||||
function switchSource(source, index) {
|
||||
currentUrl.value = source.url;
|
||||
currentSourceIndex.value = index;
|
||||
playerRef.value?.play(source.url);
|
||||
showSourceSelector.value = false;
|
||||
}
|
||||
|
||||
// 播放失败处理
|
||||
async function handlePlayError() {
|
||||
if (!currentChannel.value) return;
|
||||
|
||||
const urls = currentChannel.value.urls;
|
||||
if (urls.length <= 1) return; // 只有一个线路,无法切换
|
||||
|
||||
// 尝试下一个线路
|
||||
const nextIndex = (currentSourceIndex.value + 1) % urls.length;
|
||||
if (nextIndex === 0) {
|
||||
// 所有线路都试过了
|
||||
console.error("所有线路都失败");
|
||||
return;
|
||||
}
|
||||
|
||||
currentSourceIndex.value = nextIndex;
|
||||
currentUrl.value = urls[nextIndex];
|
||||
|
||||
// 标记当前线路失败
|
||||
const url = urls[currentSourceIndex.value];
|
||||
const cached = validityMap.value.get(url) || {};
|
||||
validityMap.value.set(url, {
|
||||
...cached,
|
||||
status: "offline",
|
||||
failCount: (cached.failCount || 0) + 1,
|
||||
});
|
||||
}
|
||||
|
||||
// 重新加载频道(供设置面板调用)
|
||||
async function reloadChannels() {
|
||||
await loadChannels(true);
|
||||
}
|
||||
|
||||
// 节目信息(TODO: 实际从 EPG 获取)
|
||||
const currentProgramTitle = ref("精彩节目");
|
||||
const programProgress = ref(0);
|
||||
const currentTime = ref("--:--");
|
||||
const totalTime = ref("--:--");
|
||||
|
||||
// 更新节目信息
|
||||
function updateProgramInfo() {
|
||||
const now = new Date();
|
||||
currentTime.value = now.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
// TODO: 从 EPG 获取当前节目
|
||||
}
|
||||
|
||||
// 线路有效性缓存
|
||||
const validityMap = ref(new Map());
|
||||
async function loadValidityCache() {
|
||||
const all = await storage.getAllValidity();
|
||||
for (const v of all) {
|
||||
validityMap.value.set(v.url, v);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await loadValidityCache();
|
||||
// 频道数据由 useChannels 自动加载
|
||||
|
||||
// 定时更新节目信息
|
||||
setInterval(updateProgramInfo, 60000);
|
||||
updateProgramInfo();
|
||||
});
|
||||
|
||||
// 监听当前频道变化,更新线路有效性
|
||||
watch(currentChannel, async (channel) => {
|
||||
if (!channel || !channel.urls) return;
|
||||
|
||||
// 检查是否需要测速
|
||||
const needCheck = channel.urls.some((url) => {
|
||||
const cached = validityMap.value.get(url);
|
||||
return !cached || Date.now() - cached.checkedAt > 60 * 60 * 1000;
|
||||
});
|
||||
|
||||
if (needCheck) {
|
||||
// 后台测速
|
||||
sortUrlsBySpeed(channel.urls);
|
||||
}
|
||||
});
|
||||
|
||||
// 遥控输入完成处理
|
||||
function handleRemoteInputComplete(value) {
|
||||
console.log("遥控输入完成:", value);
|
||||
// 根据输入值跳转到对应频道
|
||||
const channelIndex = parseInt(value, 10) - 1;
|
||||
if (channelIndex >= 0 && channelIndex < channels.value.length) {
|
||||
handlePlay(channels.value[channelIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
useKeyEvent("Enter", showLeftPanel);
|
||||
useKeyEvent("Escape", showBottomPanel);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -13,12 +13,18 @@
|
||||
</div>
|
||||
<div class="channel-info">
|
||||
<div class="channel-name-row">
|
||||
<span class="name">{{ channel?.name || '未选择频道' }}</span>
|
||||
<span class="source-tag">线路 {{ currentSourceIndex + 1 }}/{{ channel?.urls?.length || 0 }}</span>
|
||||
<span class="name">{{ channel?.name || "未选择频道" }}</span>
|
||||
<span class="source-tag"
|
||||
>线路 {{ currentSourceIndex + 1 }}/{{
|
||||
channel?.urls?.length || 0
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="program-info">
|
||||
<span class="live-dot">●</span>
|
||||
<span class="program-title">{{ currentProgram || '精彩节目' }}</span>
|
||||
<span class="program-title">{{
|
||||
currentProgram || "精彩节目"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
||||
@ -36,7 +42,7 @@
|
||||
@mouseenter="showBottomPanel"
|
||||
>
|
||||
<span class="icon">★</span>
|
||||
<span>{{ isFavorite ? '已收藏' : '收藏' }}</span>
|
||||
<span>{{ isFavorite ? "已收藏" : "收藏" }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -47,77 +53,15 @@
|
||||
<span class="icon">↻</span>
|
||||
<span>切换线路</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="handleSettings"
|
||||
@mouseenter="showBottomPanel"
|
||||
>
|
||||
<span class="icon">⚙</span>
|
||||
<span>设置</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
channel: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
currentSourceIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currentProgram: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currentTime: {
|
||||
type: String,
|
||||
default: '--:--'
|
||||
},
|
||||
totalTime: {
|
||||
type: String,
|
||||
default: '--:--'
|
||||
},
|
||||
isFavorite: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
import { useKeyEvent } from "../../composables/useEvent.js";
|
||||
|
||||
const emit = defineEmits(['favorite', 'switch-source', 'settings']);
|
||||
|
||||
import { useUI } from "../../composables/useUI.js";
|
||||
const { bottomPanelVisible, showBottomPanel } = useUI();
|
||||
|
||||
// 获取频道 LOGO
|
||||
function getChannelLogo(name) {
|
||||
return name ? name.slice(0, 2) : '--';
|
||||
}
|
||||
|
||||
// 收藏
|
||||
function handleFavorite() {
|
||||
emit('favorite');
|
||||
showBottomPanel();
|
||||
}
|
||||
|
||||
// 切换线路
|
||||
function handleSwitchSource() {
|
||||
emit('switch-source');
|
||||
}
|
||||
|
||||
// 设置
|
||||
function handleSettings() {
|
||||
emit('settings');
|
||||
}
|
||||
useKeyEvent("Escape", showBottomPanel);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -9,9 +9,10 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useEvent } from "../../composables/useEvent.js";
|
||||
import { usePlayer } from "../../composables/usePlayer.js";
|
||||
import { debounce } from "../../utils/common.js";
|
||||
|
||||
const emit = defineEmits(["complete"]);
|
||||
const { showLeftPanel } = usePlayer();
|
||||
|
||||
// 内部状态
|
||||
const visible = ref(false);
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 第三栏:日期列表 -->
|
||||
<div class="column column-3" :class="{ active: activeColumn === 2 }">
|
||||
<!-- <div class="column column-3" :class="{ active: activeColumn === 2 }">
|
||||
<div class="date-list" :class="{ 'is-active': activeColumn === 2 }">
|
||||
<div
|
||||
v-for="date in dates"
|
||||
@ -91,10 +91,10 @@
|
||||
<div class="date-label">{{ date.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 第四栏:节目单列表 -->
|
||||
<div class="column column-4" :class="{ active: activeColumn === 3 }">
|
||||
<!-- <div class="column column-4" :class="{ active: activeColumn === 3 }">
|
||||
<div class="program-list" :class="{ 'is-active': activeColumn === 3 }">
|
||||
<div
|
||||
v-for="program in programs"
|
||||
@ -113,102 +113,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useUI } from "../../composables/useUI.js";
|
||||
import { useKeyEvent } from "../../composables/useEvent.js";
|
||||
import { useGroups } from "../../composables/useGroups.js";
|
||||
import { useChannelFilter } from "../../composables/useChannelFilter.js";
|
||||
import { useDates } from "../../composables/useDates.js";
|
||||
import { usePrograms } from "../../composables/usePrograms.js";
|
||||
import { useFavorites } from "../../composables/useFavorites.js";
|
||||
import { useChannels } from "../../composables/useChannels.js";
|
||||
|
||||
const props = defineProps({
|
||||
currentChannel: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
validityMap: {
|
||||
type: Map,
|
||||
default: () => new Map(),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["play"]);
|
||||
|
||||
const { isTV, leftPanelVisible, hideLeftPanel, currentActiveColumn } = useUI();
|
||||
const activeColumn = computed(() => currentActiveColumn.value);
|
||||
|
||||
// 从 hooks 获取数据
|
||||
const { favorites } = useFavorites();
|
||||
const { channels, groups } = useChannels();
|
||||
|
||||
// ===== 分组逻辑 =====
|
||||
const {
|
||||
selectedGroup,
|
||||
pinnedGroups,
|
||||
normalGroups,
|
||||
selectGroup,
|
||||
setGroups,
|
||||
initSelectedGroup,
|
||||
} = useGroups();
|
||||
const { selectedGroup, pinnedGroups, normalGroups, selectGroup } = useGroups();
|
||||
|
||||
// ===== 频道过滤逻辑 =====
|
||||
const {
|
||||
selectedChannel,
|
||||
filteredChannels,
|
||||
selectChannel,
|
||||
setChannels,
|
||||
setSelectedGroup: setChannelFilterGroup,
|
||||
setFavorites,
|
||||
setValidityMap,
|
||||
getFirstChannel,
|
||||
getChannelLogo,
|
||||
isFavorite,
|
||||
getValidCount,
|
||||
} = useChannelFilter();
|
||||
|
||||
// ===== 日期逻辑 =====
|
||||
const { selectedDate, dates, selectDate, initToday } = useDates();
|
||||
|
||||
// ===== 节目单逻辑 =====
|
||||
const { selectedProgram, programs, selectProgram, loadProgramsByDate } =
|
||||
usePrograms();
|
||||
|
||||
// 同步 hooks 数据到过滤逻辑
|
||||
watch(groups, (g) => setGroups(g), { immediate: true });
|
||||
|
||||
watch(channels, (c) => setChannels(c), { immediate: true });
|
||||
|
||||
// 同步收藏到频道过滤
|
||||
watch(favorites, (favs) => setFavorites(favs), { immediate: true });
|
||||
|
||||
watch(
|
||||
() => props.validityMap,
|
||||
(map) => setValidityMap(map),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 同步选中分组到频道过滤
|
||||
watch(selectedGroup, (group) => {
|
||||
setChannelFilterGroup(group);
|
||||
});
|
||||
|
||||
// 监听面板显示,初始化选中状态
|
||||
watch(leftPanelVisible, (val) => {
|
||||
if (val && props.currentChannel) {
|
||||
initSelectedGroup(props.currentChannel, groups.value[0]);
|
||||
selectChannel(props.currentChannel);
|
||||
initToday();
|
||||
}
|
||||
});
|
||||
|
||||
// 分组选择
|
||||
function onGroupSelect(groupId) {
|
||||
selectGroup(groupId);
|
||||
@ -226,19 +161,7 @@ function onChannelSelect(channel) {
|
||||
hideLeftPanel();
|
||||
}
|
||||
|
||||
// 日期选择
|
||||
function onDateSelect(dateValue) {
|
||||
selectDate(dateValue);
|
||||
loadProgramsByDate(dateValue);
|
||||
}
|
||||
|
||||
// 节目选择
|
||||
function onProgramSelect(program) {
|
||||
selectProgram(program);
|
||||
// TODO: 实现回看逻辑
|
||||
hideLeftPanel();
|
||||
}
|
||||
|
||||
useKeyEvent("Enter", showLeftPanel);
|
||||
useKeyEvent("Escape", hideLeftPanel);
|
||||
</script>
|
||||
|
||||
|
||||
24
ui/src/composables/useApp.js
Normal file
24
ui/src/composables/useApp.js
Normal file
@ -0,0 +1,24 @@
|
||||
// 默认设置
|
||||
const defaultSettings = {
|
||||
bottomPanelCloseDelay: 3000, // 底部面板自动关闭延迟(毫秒)
|
||||
leftPanelCloseDelay: 500, // 左侧面板自动关闭延迟(毫秒)
|
||||
inputPanelCloseDelay: 2000, // 输入面板自动关闭延迟(毫秒)
|
||||
};
|
||||
|
||||
const settings = ref({ ...defaultSettings });
|
||||
|
||||
export const useApp = () => {
|
||||
const { getSettings } = useStorage();
|
||||
|
||||
const initSetting = async () => {
|
||||
const saved = await getSettings();
|
||||
if (saved) {
|
||||
settings.value = { ...defaultSettings, ...saved };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
settings,
|
||||
initSetting,
|
||||
};
|
||||
};
|
||||
@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 频道检测结果缓存
|
||||
* 避免每次打开都重复检测
|
||||
*/
|
||||
|
||||
const CACHE_KEY = 'iptv_channel_cache'
|
||||
const CACHE_VERSION = '1.0'
|
||||
const DEFAULT_EXPIRE = 24 * 60 * 60 * 1000 // 默认缓存24小时
|
||||
|
||||
// 获取缓存
|
||||
export function getCachedChannels() {
|
||||
try {
|
||||
const saved = localStorage.getItem(CACHE_KEY)
|
||||
if (!saved) return null
|
||||
|
||||
const { version, timestamp, data } = JSON.parse(saved)
|
||||
|
||||
// 版本检查
|
||||
if (version !== CACHE_VERSION) {
|
||||
console.log('[Cache] 版本过期,清除缓存')
|
||||
clearCache()
|
||||
return null
|
||||
}
|
||||
|
||||
// 过期检查(使用用户配置的过期时间)
|
||||
const expireTime = parseInt(localStorage.getItem('iptv_cache_expire')) || DEFAULT_EXPIRE
|
||||
const age = Date.now() - timestamp
|
||||
if (age > expireTime) {
|
||||
console.log('[Cache] 缓存已过期,年龄:', Math.round(age / 1000 / 60), '分钟')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('[Cache] 命中缓存,年龄:', Math.round(age / 1000 / 60), '分钟,剩余:', Math.round((expireTime - age) / 1000 / 60), '分钟')
|
||||
return data
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Cache] 读取失败:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 设置缓存
|
||||
export function setCachedChannels(channels) {
|
||||
try {
|
||||
const cache = {
|
||||
version: CACHE_VERSION,
|
||||
timestamp: Date.now(),
|
||||
data: channels
|
||||
}
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cache))
|
||||
console.log('[Cache] 已保存', channels.length, '个频道')
|
||||
} catch (e) {
|
||||
console.error('[Cache] 保存失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
export function clearCache() {
|
||||
localStorage.removeItem(CACHE_KEY)
|
||||
console.log('[Cache] 已清除')
|
||||
}
|
||||
|
||||
// 检查是否有有效缓存
|
||||
export function hasValidCache() {
|
||||
return getCachedChannels() !== null
|
||||
}
|
||||
|
||||
// 获取缓存信息
|
||||
export function getCacheInfo() {
|
||||
try {
|
||||
const saved = localStorage.getItem(CACHE_KEY)
|
||||
if (!saved) return null
|
||||
|
||||
const { timestamp } = JSON.parse(saved)
|
||||
const expireTime = parseInt(localStorage.getItem('iptv_cache_expire')) || DEFAULT_EXPIRE
|
||||
const age = Date.now() - timestamp
|
||||
const remaining = expireTime - age
|
||||
|
||||
return {
|
||||
age: Math.max(0, Math.round(age / 1000 / 60)), // 已缓存分钟数
|
||||
remaining: Math.max(0, Math.round(remaining / 1000 / 60)), // 剩余分钟数
|
||||
isValid: remaining > 0
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export function useChannelFilter() {
|
||||
const selectedChannel = ref(null);
|
||||
const allChannels = ref([]);
|
||||
const selectedGroup = ref("");
|
||||
const favorites = ref(new Set());
|
||||
const validityMap = ref(new Map());
|
||||
|
||||
// 过滤后的频道
|
||||
const filteredChannels = computed(() => {
|
||||
if (!selectedGroup.value) return allChannels.value;
|
||||
|
||||
// 置顶分组特殊处理
|
||||
if (selectedGroup.value === "recent") {
|
||||
// TODO: 返回最近播放
|
||||
return [];
|
||||
}
|
||||
if (selectedGroup.value === "favorite") {
|
||||
return allChannels.value.filter((c) => favorites.value.has(c.id));
|
||||
}
|
||||
|
||||
return allChannels.value.filter((c) => c.group === selectedGroup.value);
|
||||
});
|
||||
|
||||
// 获取频道 LOGO(取前两个字符)
|
||||
function getChannelLogo(name) {
|
||||
return name?.slice(0, 2) || "--";
|
||||
}
|
||||
|
||||
// 检查是否收藏
|
||||
function isFavorite(channelId) {
|
||||
return favorites.value.has(channelId);
|
||||
}
|
||||
|
||||
// 获取有效线路数
|
||||
function getValidCount(channel) {
|
||||
if (!channel?.urls) return 0;
|
||||
return channel.urls.filter((url) => {
|
||||
const validity = validityMap.value.get(url);
|
||||
return validity?.status === "online";
|
||||
}).length;
|
||||
}
|
||||
|
||||
// 选择频道
|
||||
function selectChannel(channel) {
|
||||
selectedChannel.value = channel;
|
||||
return channel;
|
||||
}
|
||||
|
||||
// 设置数据源
|
||||
function setChannels(channels) {
|
||||
allChannels.value = channels;
|
||||
}
|
||||
|
||||
function setSelectedGroup(group) {
|
||||
selectedGroup.value = group;
|
||||
}
|
||||
|
||||
function setFavorites(favSet) {
|
||||
favorites.value = favSet;
|
||||
}
|
||||
|
||||
function setValidityMap(map) {
|
||||
validityMap.value = map;
|
||||
}
|
||||
|
||||
// 根据当前频道初始化
|
||||
function initSelectedChannel(channel) {
|
||||
if (channel) {
|
||||
selectedChannel.value = channel;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取第一个频道(用于分组切换时自动选中)
|
||||
function getFirstChannel() {
|
||||
return filteredChannels.value[0] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedChannel,
|
||||
filteredChannels,
|
||||
selectChannel,
|
||||
setChannels,
|
||||
setSelectedGroup,
|
||||
setFavorites,
|
||||
setValidityMap,
|
||||
initSelectedChannel,
|
||||
getFirstChannel,
|
||||
getChannelLogo,
|
||||
isFavorite,
|
||||
getValidCount,
|
||||
};
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useStorage } from "./useStorage.js";
|
||||
|
||||
// 全局状态
|
||||
const channels = ref([]);
|
||||
const loading = ref(false);
|
||||
let initialized = false;
|
||||
|
||||
export function useChannels() {
|
||||
const storage = useStorage();
|
||||
|
||||
// 分组列表(基于频道计算)
|
||||
const groups = computed(() => {
|
||||
return [...new Set(channels.value.map((c) => c.group))];
|
||||
});
|
||||
|
||||
// 加载频道数据
|
||||
const loadChannels = async (force = false) => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 检查缓存
|
||||
if (!force) {
|
||||
const isValid = await storage.isListCacheValid();
|
||||
if (isValid) {
|
||||
const cached = await storage.getChannels();
|
||||
if (cached && cached.length > 0) {
|
||||
channels.value = cached;
|
||||
loading.value = false;
|
||||
// 后台刷新
|
||||
refreshChannelsInBackground();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从网络加载
|
||||
const text = await fetchChannelData();
|
||||
if (text && !text.startsWith("ERROR")) {
|
||||
const parsed = parseChannelData(text);
|
||||
channels.value = parsed;
|
||||
await storage.setChannels(parsed);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 后台刷新频道
|
||||
const refreshChannelsInBackground = async () => {
|
||||
try {
|
||||
const text = await fetchChannelData();
|
||||
if (text && !text.startsWith("ERROR")) {
|
||||
const parsed = parseChannelData(text);
|
||||
if (parsed.length > 0) {
|
||||
channels.value = parsed;
|
||||
await storage.setChannels(parsed);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("后台刷新失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取频道数据(网络)
|
||||
const fetchChannelData = async () => {
|
||||
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||
|
||||
// Android/TV 使用原生接口
|
||||
if ((platform === "android" || platform === "tv") && window.AndroidAsset) {
|
||||
try {
|
||||
return window.AndroidAsset.readChannelData();
|
||||
} catch (e) {
|
||||
console.error("读取本地数据失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Web/Desktop 从网络加载
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/output/result.txt"
|
||||
);
|
||||
if (response.ok) {
|
||||
return await response.text();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("网络加载失败:", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 解析频道数据
|
||||
const parseChannelData = (text) => {
|
||||
const result = [];
|
||||
const lines = text.split("\n");
|
||||
let currentGroup = "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.includes("#genre#")) {
|
||||
currentGroup = trimmed.split(",")[0];
|
||||
} else if (trimmed.includes(",")) {
|
||||
const [name, url] = trimmed.split(",").map((s) => s.trim());
|
||||
if (name && url) {
|
||||
const existing = result.find(
|
||||
(c) => c.name === name && c.group === currentGroup
|
||||
);
|
||||
if (existing) {
|
||||
existing.urls.push(url);
|
||||
} else {
|
||||
result.push({
|
||||
id: `${currentGroup}_${name}`,
|
||||
name,
|
||||
group: currentGroup,
|
||||
urls: [url],
|
||||
logo: "",
|
||||
epgId: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 根据索引获取频道
|
||||
const getChannelByIndex = (index) => {
|
||||
return channels.value[index] || null;
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
if (!initialized) {
|
||||
loadChannels();
|
||||
initialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
channels,
|
||||
groups,
|
||||
loading,
|
||||
loadChannels,
|
||||
getChannelByIndex,
|
||||
};
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 日期标签
|
||||
const DAY_LABELS = ["今天", "明天", "后天"];
|
||||
|
||||
export function useDates() {
|
||||
const selectedDate = ref("");
|
||||
|
||||
// 生成日期列表(今天、明天、后天...)
|
||||
const dates = computed(() => {
|
||||
const list = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
|
||||
const value = date.toISOString().split("T")[0];
|
||||
const day = `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
const label =
|
||||
i < 3
|
||||
? DAY_LABELS[i]
|
||||
: `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
|
||||
list.push({ value, day, label });
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
// 选择日期
|
||||
function selectDate(dateValue) {
|
||||
selectedDate.value = dateValue;
|
||||
}
|
||||
|
||||
// 初始化为今天
|
||||
function initToday() {
|
||||
selectedDate.value = new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
return {
|
||||
selectedDate,
|
||||
dates,
|
||||
selectDate,
|
||||
initToday,
|
||||
};
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useStorage } from "./useStorage.js";
|
||||
|
||||
const STORAGE_KEY = "favorites_data";
|
||||
|
||||
// 全局状态
|
||||
const favorites = ref(new Set());
|
||||
let initialized = false;
|
||||
let loadPromise = null;
|
||||
|
||||
export function useFavorites() {
|
||||
const storage = useStorage();
|
||||
|
||||
// 加载收藏(从 useStorage)
|
||||
const loadFavorites = async () => {
|
||||
if (loadPromise) return loadPromise;
|
||||
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const saved = await storage.get(STORAGE_KEY);
|
||||
if (saved) {
|
||||
favorites.value = new Set(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载收藏失败:", e);
|
||||
}
|
||||
initialized = true;
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
};
|
||||
|
||||
// 保存收藏(到 useStorage)
|
||||
const saveFavorites = async () => {
|
||||
try {
|
||||
await storage.set(STORAGE_KEY, [...favorites.value]);
|
||||
} catch (e) {
|
||||
console.error("保存收藏失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换收藏状态
|
||||
const toggleFavorite = async (channelId) => {
|
||||
if (!channelId) return;
|
||||
if (favorites.value.has(channelId)) {
|
||||
favorites.value.delete(channelId);
|
||||
} else {
|
||||
favorites.value.add(channelId);
|
||||
}
|
||||
await saveFavorites();
|
||||
};
|
||||
|
||||
// 检查是否已收藏
|
||||
const isFavorite = (channelId) => {
|
||||
return favorites.value.has(channelId);
|
||||
};
|
||||
|
||||
// 获取所有收藏
|
||||
const getFavorites = () => {
|
||||
return [...favorites.value];
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
if (!initialized) {
|
||||
loadFavorites();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
favorites,
|
||||
toggleFavorite,
|
||||
isFavorite,
|
||||
getFavorites,
|
||||
};
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 置顶分组配置
|
||||
const PINNED_GROUPS = [
|
||||
{ id: "recent", name: "最近播放", icon: "⏱" },
|
||||
{ id: "favorite", name: "收藏", icon: "❤️" },
|
||||
];
|
||||
|
||||
// 分组图标映射
|
||||
const GROUP_ICONS = {
|
||||
央视: "📺",
|
||||
卫视: "📡",
|
||||
体育: "⚽",
|
||||
电影: "🎬",
|
||||
少儿: "👶",
|
||||
};
|
||||
|
||||
export function useGroups() {
|
||||
const selectedGroup = ref("");
|
||||
const rawGroups = ref([]);
|
||||
|
||||
// 计算置顶分组(带数量)
|
||||
const pinnedGroups = computed(() => {
|
||||
return PINNED_GROUPS.map((g) => ({
|
||||
...g,
|
||||
count: 0, // TODO: 实际数量从外部传入
|
||||
}));
|
||||
});
|
||||
|
||||
// 计算普通分组(带图标和数量)
|
||||
const normalGroups = computed(() => {
|
||||
return rawGroups.value.map((group) => ({
|
||||
id: group,
|
||||
name: group,
|
||||
icon: getGroupIcon(group),
|
||||
count: 0, // TODO: 实际数量从外部传入
|
||||
}));
|
||||
});
|
||||
|
||||
// 所有分组
|
||||
const allGroups = computed(() => [...pinnedGroups.value, ...normalGroups.value]);
|
||||
|
||||
// 获取分组图标
|
||||
function getGroupIcon(groupName) {
|
||||
for (const [key, icon] of Object.entries(GROUP_ICONS)) {
|
||||
if (groupName.includes(key)) return icon;
|
||||
}
|
||||
return "📺";
|
||||
}
|
||||
|
||||
// 选择分组
|
||||
function selectGroup(groupId) {
|
||||
selectedGroup.value = groupId;
|
||||
}
|
||||
|
||||
// 设置原始分组数据
|
||||
function setGroups(groups) {
|
||||
rawGroups.value = groups;
|
||||
}
|
||||
|
||||
// 根据当前频道初始化选中分组
|
||||
function initSelectedGroup(channel, defaultGroup = "") {
|
||||
if (channel?.group) {
|
||||
selectedGroup.value = channel.group;
|
||||
} else if (defaultGroup) {
|
||||
selectedGroup.value = defaultGroup;
|
||||
} else if (rawGroups.value.length > 0) {
|
||||
selectedGroup.value = rawGroups.value[0];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedGroup,
|
||||
pinnedGroups,
|
||||
normalGroups,
|
||||
allGroups,
|
||||
selectGroup,
|
||||
setGroups,
|
||||
initSelectedGroup,
|
||||
getGroupIcon,
|
||||
};
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useStorage } from "./useStorage.js";
|
||||
|
||||
const STORAGE_KEY = "play_history";
|
||||
const MAX_HISTORY = 20;
|
||||
|
||||
// 全局状态
|
||||
const history = ref([]);
|
||||
let initialized = false;
|
||||
let loadPromise = null;
|
||||
|
||||
export function useHistory() {
|
||||
const storage = useStorage();
|
||||
|
||||
// 加载历史(从 useStorage)
|
||||
const loadHistory = async () => {
|
||||
if (loadPromise) return loadPromise;
|
||||
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const saved = await storage.get(STORAGE_KEY);
|
||||
if (saved) {
|
||||
history.value = saved;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载历史失败:", e);
|
||||
}
|
||||
initialized = true;
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
};
|
||||
|
||||
// 保存历史(到 useStorage)
|
||||
const saveHistory = async () => {
|
||||
try {
|
||||
await storage.set(STORAGE_KEY, [...history.value]);
|
||||
} catch (e) {
|
||||
console.error("保存历史失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加到历史
|
||||
const addToHistory = async (channel) => {
|
||||
if (!channel) return;
|
||||
// 移除重复项
|
||||
history.value = history.value.filter((h) => h.id !== channel.id);
|
||||
// 添加到开头
|
||||
history.value.unshift({
|
||||
...channel,
|
||||
playedAt: Date.now(),
|
||||
});
|
||||
// 限制数量
|
||||
if (history.value.length > MAX_HISTORY) {
|
||||
history.value = history.value.slice(0, MAX_HISTORY);
|
||||
}
|
||||
await saveHistory();
|
||||
};
|
||||
|
||||
// 清空历史
|
||||
const clearHistory = async () => {
|
||||
history.value = [];
|
||||
await saveHistory();
|
||||
};
|
||||
|
||||
// 获取历史列表
|
||||
const getHistory = () => {
|
||||
return [...history.value];
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
if (!initialized) {
|
||||
loadHistory();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
history,
|
||||
addToHistory,
|
||||
clearHistory,
|
||||
getHistory,
|
||||
};
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 移动端检测
|
||||
*/
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useMobile() {
|
||||
const isMobile = ref(false)
|
||||
const isTouch = ref(false)
|
||||
|
||||
const checkMobile = () => {
|
||||
// 检测移动设备
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
const mobileKeywords = [
|
||||
'android', 'iphone', 'ipad', 'ipod', 'windows phone',
|
||||
'mobile', 'mobi', 'tablet'
|
||||
]
|
||||
|
||||
isMobile.value = mobileKeywords.some(keyword =>
|
||||
userAgent.includes(keyword)
|
||||
) || window.innerWidth < 768
|
||||
|
||||
// 检测触摸设备
|
||||
isTouch.value = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
checkMobile()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isTouch
|
||||
}
|
||||
}
|
||||
60
ui/src/composables/usePlayer.js
Normal file
60
ui/src/composables/usePlayer.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { useApp } from "./useApp";
|
||||
import { debounce } from "../utils/common";
|
||||
|
||||
const currentGroup = ref();
|
||||
const currentChannel = ref();
|
||||
const currentSource = ref();
|
||||
const isShowLeftPanel = ref(false);
|
||||
const isShowBottomPanel = ref(false);
|
||||
const isShowInputPanel = ref(false);
|
||||
const isShowDebugPanel = ref(false);
|
||||
|
||||
export const usePlayer = () => {
|
||||
const { settings } = useApp();
|
||||
|
||||
const setCurrentGroup = (group) => {
|
||||
currentGroup.value = group;
|
||||
};
|
||||
|
||||
const setCurrentChannel = (channel) => {
|
||||
currentChannel.value = channel;
|
||||
};
|
||||
|
||||
const setCurrentSource = (source) => {
|
||||
currentSource.value = source;
|
||||
};
|
||||
|
||||
const showLeftPanel = () => {
|
||||
isShowLeftPanel.value = true;
|
||||
};
|
||||
|
||||
const closeLeftPanel = () => {
|
||||
isShowLeftPanel.value = false;
|
||||
};
|
||||
|
||||
const showBottomPanel = () => {
|
||||
isShowBottomPanel.value = true;
|
||||
closeBottomPanel();
|
||||
};
|
||||
|
||||
const closeBottomPanel = debounce(() => {
|
||||
isShowBottomPanel.value = false;
|
||||
}, settings.value.bottomPanelCloseDelay);
|
||||
|
||||
return {
|
||||
currentGroup,
|
||||
setCurrentGroup,
|
||||
currentChannel,
|
||||
setCurrentChannel,
|
||||
currentSource,
|
||||
setCurrentSource,
|
||||
|
||||
isShowLeftPanel,
|
||||
showLeftPanel,
|
||||
closeLeftPanel,
|
||||
|
||||
isShowBottomPanel,
|
||||
showBottomPanel,
|
||||
closeBottomPanel,
|
||||
};
|
||||
};
|
||||
@ -1,42 +0,0 @@
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 模拟节目单数据(TODO: 实际从 EPG 获取)
|
||||
const MOCK_PROGRAMS = [
|
||||
{ id: 1, time: "08:00", title: "朝闻天下", isCurrent: false },
|
||||
{ id: 2, time: "09:00", title: "今日说法", isCurrent: false },
|
||||
{ id: 3, time: "10:00", title: "电视剧:繁花", isCurrent: true },
|
||||
{ id: 4, time: "12:00", title: "新闻30分", isCurrent: false },
|
||||
{ id: 5, time: "13:00", title: "电视剧:西游记", isCurrent: false },
|
||||
{ id: 6, time: "15:00", title: "动画剧场", isCurrent: false },
|
||||
{ id: 7, time: "19:00", title: "新闻联播", isCurrent: false },
|
||||
];
|
||||
|
||||
export function usePrograms() {
|
||||
const selectedProgram = ref(null);
|
||||
const programs = ref([...MOCK_PROGRAMS]);
|
||||
|
||||
// 选择节目
|
||||
function selectProgram(program) {
|
||||
selectedProgram.value = program;
|
||||
return program;
|
||||
}
|
||||
|
||||
// 加载指定日期的节目(TODO: 实际从 EPG 获取)
|
||||
function loadProgramsByDate(dateValue) {
|
||||
// TODO: 根据日期加载节目单
|
||||
console.log("加载节目单:", dateValue);
|
||||
}
|
||||
|
||||
// 更新节目单
|
||||
function setPrograms(newPrograms) {
|
||||
programs.value = newPrograms;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedProgram,
|
||||
programs,
|
||||
selectProgram,
|
||||
loadProgramsByDate,
|
||||
setPrograms,
|
||||
};
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useStorage } from "./useStorage.js";
|
||||
|
||||
const STORAGE_KEY = "user_settings";
|
||||
|
||||
// 默认设置
|
||||
const defaultSettings = {
|
||||
autoPlay: true,
|
||||
defaultVolume: 0.8,
|
||||
showEpg: true,
|
||||
theme: "dark",
|
||||
checkTimeout: 2000, // 检测超时时间(ms)
|
||||
checkConcurrency: 5, // 并发数
|
||||
};
|
||||
|
||||
// 全局状态
|
||||
const settings = ref({ ...defaultSettings });
|
||||
let initialized = false;
|
||||
let loadPromise = null;
|
||||
|
||||
export function useSettings() {
|
||||
const storage = useStorage();
|
||||
|
||||
// 加载设置(从 useStorage)
|
||||
const loadSettings = async () => {
|
||||
if (loadPromise) return loadPromise;
|
||||
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
const saved = await storage.get(STORAGE_KEY);
|
||||
if (saved) {
|
||||
settings.value = { ...defaultSettings, ...saved };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载设置失败:", e);
|
||||
}
|
||||
initialized = true;
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
};
|
||||
|
||||
// 保存设置(到 useStorage)
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
await storage.set(STORAGE_KEY, settings.value);
|
||||
} catch (e) {
|
||||
console.error("保存设置失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新单个设置
|
||||
const updateSetting = async (key, value) => {
|
||||
if (key in settings.value) {
|
||||
settings.value[key] = value;
|
||||
await saveSettings();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新多个设置
|
||||
const updateSettings = async (newSettings) => {
|
||||
settings.value = { ...settings.value, ...newSettings };
|
||||
await saveSettings();
|
||||
};
|
||||
|
||||
// 重置设置为默认值
|
||||
const resetSettings = async () => {
|
||||
settings.value = { ...defaultSettings };
|
||||
await saveSettings();
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
if (!initialized) {
|
||||
loadSettings();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
settings,
|
||||
updateSetting,
|
||||
updateSettings,
|
||||
resetSettings,
|
||||
};
|
||||
}
|
||||
@ -1,19 +1,10 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import createStorage, { Subscription, Channel, SourceValidity, Preferences, PlayHistory, CacheMeta } from '../storage/index.js';
|
||||
import { ref, onMounted } from "vue";
|
||||
import createStorage from "../storage/index.js";
|
||||
|
||||
const storage = createStorage();
|
||||
const isReady = ref(false);
|
||||
|
||||
export { Subscription, Channel, SourceValidity, Preferences, PlayHistory, CacheMeta };
|
||||
|
||||
export function useStorage() {
|
||||
onMounted(async () => {
|
||||
if (storage.init) {
|
||||
await storage.init();
|
||||
}
|
||||
isReady.value = true;
|
||||
});
|
||||
|
||||
// 基础操作
|
||||
const get = async (key) => {
|
||||
return await storage.get(key);
|
||||
@ -27,143 +18,28 @@ export function useStorage() {
|
||||
return await storage.remove(key);
|
||||
};
|
||||
|
||||
// 频道数据
|
||||
const getChannels = async () => {
|
||||
const channels = await storage.getChannels();
|
||||
return channels.map(c => new Channel(c));
|
||||
};
|
||||
|
||||
const setChannels = async (channels) => {
|
||||
return await storage.setChannels(channels);
|
||||
};
|
||||
|
||||
const getGroups = async () => {
|
||||
return await storage.getGroups();
|
||||
};
|
||||
|
||||
// 检查列表缓存是否有效
|
||||
const isListCacheValid = async () => {
|
||||
const prefs = await storage.getPreferences();
|
||||
const ttl = prefs.listCacheTTL || 24 * 60 * 60 * 1000;
|
||||
return await storage.isCacheValid('channels', ttl);
|
||||
};
|
||||
|
||||
// 线路有效性
|
||||
const getValidity = async (url) => {
|
||||
const v = await storage.getValidity(url);
|
||||
return v ? new SourceValidity(v) : null;
|
||||
};
|
||||
|
||||
const setValidity = async (url, validity) => {
|
||||
return await storage.setValidity(url, validity);
|
||||
};
|
||||
|
||||
const getAllValidity = async () => {
|
||||
const list = await storage.getAllValidity();
|
||||
return list.map(v => new SourceValidity(v));
|
||||
};
|
||||
|
||||
// 检查有效性缓存是否有效
|
||||
const isValidityCacheValid = async (url) => {
|
||||
const prefs = await storage.getPreferences();
|
||||
const ttl = prefs.validityCacheTTL || 12 * 60 * 60 * 1000;
|
||||
const validity = await storage.getValidity(url);
|
||||
if (!validity || !validity.checkedAt) return false;
|
||||
return Date.now() - validity.checkedAt < ttl;
|
||||
};
|
||||
|
||||
// 用户偏好
|
||||
const getPreferences = async () => {
|
||||
const prefs = await storage.getPreferences();
|
||||
return new Preferences(prefs);
|
||||
};
|
||||
|
||||
const setPreferences = async (prefs) => {
|
||||
return await storage.setPreferences(prefs);
|
||||
};
|
||||
|
||||
// 播放历史
|
||||
const getHistory = async (limit = 50) => {
|
||||
const list = await storage.getHistory(limit);
|
||||
return list.map(h => new PlayHistory(h));
|
||||
};
|
||||
|
||||
const addHistory = async (item) => {
|
||||
return await storage.addHistory(item);
|
||||
};
|
||||
|
||||
const clearHistory = async () => {
|
||||
return await storage.clearHistory();
|
||||
};
|
||||
|
||||
// 订阅源
|
||||
const getSubscriptions = async () => {
|
||||
const list = await storage.getSubscriptions();
|
||||
return list.map(s => new Subscription(s));
|
||||
};
|
||||
|
||||
const setSubscriptions = async (subs) => {
|
||||
return await storage.setSubscriptions(subs);
|
||||
};
|
||||
|
||||
// 缓存元数据
|
||||
const getCacheMeta = async (key) => {
|
||||
const meta = await storage.getCacheMeta(key);
|
||||
return meta ? new CacheMeta(meta) : null;
|
||||
};
|
||||
|
||||
const setCacheMeta = async (key, meta) => {
|
||||
return await storage.setCacheMeta(key, meta);
|
||||
};
|
||||
|
||||
const isCacheValid = async (key, ttl) => {
|
||||
return await storage.isCacheValid(key, ttl);
|
||||
};
|
||||
|
||||
// 清空所有数据
|
||||
const clear = async () => {
|
||||
return await storage.clear();
|
||||
};
|
||||
|
||||
// 频道数据
|
||||
const getGroups = async () => {
|
||||
return await storage.getGroups();
|
||||
};
|
||||
|
||||
const getChannels = async () => {
|
||||
const channels = await storage.getChannels();
|
||||
return channels.map((c) => new Channel(c));
|
||||
};
|
||||
|
||||
return {
|
||||
isReady,
|
||||
// 基础
|
||||
get,
|
||||
set,
|
||||
remove,
|
||||
clear,
|
||||
// 频道
|
||||
getChannels,
|
||||
setChannels,
|
||||
getGroups,
|
||||
isListCacheValid,
|
||||
// 有效性
|
||||
getValidity,
|
||||
setValidity,
|
||||
getAllValidity,
|
||||
isValidityCacheValid,
|
||||
// 偏好
|
||||
getPreferences,
|
||||
setPreferences,
|
||||
// 历史
|
||||
getHistory,
|
||||
addHistory,
|
||||
clearHistory,
|
||||
// 订阅
|
||||
getSubscriptions,
|
||||
setSubscriptions,
|
||||
// 元数据
|
||||
getCacheMeta,
|
||||
setCacheMeta,
|
||||
isCacheValid,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建单例
|
||||
let storageInstance = null;
|
||||
export function useStorageSingleton() {
|
||||
if (!storageInstance) {
|
||||
storageInstance = useStorage();
|
||||
}
|
||||
return storageInstance;
|
||||
}
|
||||
|
||||
18
ui/src/composables/useStore.js
Normal file
18
ui/src/composables/useStore.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { useStorage } from "./useStorage";
|
||||
|
||||
const groups = ref([]);
|
||||
const channels = ref([]);
|
||||
|
||||
export const useStore = () => {
|
||||
const initStore = () => {
|
||||
const { getGroups, getChannels } = useStorage();
|
||||
groups.value = getGroups();
|
||||
channels.value = getChannels();
|
||||
};
|
||||
|
||||
return {
|
||||
groups,
|
||||
channels,
|
||||
initStore,
|
||||
};
|
||||
};
|
||||
@ -1,109 +0,0 @@
|
||||
import { ref, computed } from "vue";
|
||||
import { debounce } from "../utils/common.js";
|
||||
|
||||
// 当前激活的栏索引(用于TV导航)
|
||||
const activeColumnIndex = ref(0);
|
||||
const leftPanelVisible = ref(false);
|
||||
const bottomPanelVisible = ref(false);
|
||||
|
||||
// 当前选中的分组/频道/日期/节目
|
||||
const selectedGroup = ref("");
|
||||
const selectedChannel = ref(null);
|
||||
const selectedDate = ref("");
|
||||
const selectedProgram = ref(null);
|
||||
|
||||
export function useUI() {
|
||||
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||
const isTV = platform === "tv";
|
||||
|
||||
// 显示左侧面板
|
||||
const showLeftPanel = () => {
|
||||
leftPanelVisible.value = true;
|
||||
bottomPanelVisible.value = false;
|
||||
activeColumnIndex.value = 0;
|
||||
};
|
||||
|
||||
// 隐藏左侧面板
|
||||
const hideLeftPanel = () => {
|
||||
leftPanelVisible.value = false;
|
||||
activeColumnIndex.value = 0;
|
||||
};
|
||||
|
||||
// 切换左侧面板
|
||||
const toggleLeftPanel = () => {
|
||||
if (leftPanelVisible.value) {
|
||||
hideLeftPanel();
|
||||
} else {
|
||||
showLeftPanel();
|
||||
}
|
||||
};
|
||||
|
||||
// 显示底部栏(启动防抖隐藏)
|
||||
const showBottomPanel = () => {
|
||||
bottomPanelVisible.value = true;
|
||||
hideBottomPanel();
|
||||
};
|
||||
|
||||
// 隐藏底部栏
|
||||
const hideBottomPanel = debounce(() => {
|
||||
bottomPanelVisible.value = false;
|
||||
}, 3000);
|
||||
|
||||
// 设置选中项
|
||||
const setSelectedGroup = (group) => {
|
||||
selectedGroup.value = group;
|
||||
};
|
||||
|
||||
const setSelectedChannel = (channel) => {
|
||||
selectedChannel.value = channel;
|
||||
};
|
||||
|
||||
const setSelectedDate = (date) => {
|
||||
selectedDate.value = date;
|
||||
};
|
||||
|
||||
const setSelectedProgram = (program) => {
|
||||
selectedProgram.value = program;
|
||||
};
|
||||
|
||||
// TV 导航:切换栏
|
||||
const moveColumn = (direction) => {
|
||||
if (!leftPanelVisible.value) return;
|
||||
|
||||
const maxIndex = 3; // 0-3 四栏
|
||||
if (direction === "right") {
|
||||
activeColumnIndex.value = Math.min(activeColumnIndex.value + 1, maxIndex);
|
||||
} else if (direction === "left") {
|
||||
activeColumnIndex.value = Math.max(activeColumnIndex.value - 1, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// TV 导航:获取当前激活的栏索引
|
||||
const currentActiveColumn = computed(() => activeColumnIndex.value);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
leftPanelVisible,
|
||||
bottomPanelVisible,
|
||||
selectedGroup,
|
||||
selectedChannel,
|
||||
selectedDate,
|
||||
selectedProgram,
|
||||
|
||||
// 计算属性
|
||||
isTV,
|
||||
currentActiveColumn,
|
||||
|
||||
// 方法
|
||||
showLeftPanel,
|
||||
hideLeftPanel,
|
||||
toggleLeftPanel,
|
||||
showBottomPanel,
|
||||
hideBottomPanel,
|
||||
setSelectedGroup,
|
||||
setSelectedChannel,
|
||||
setSelectedDate,
|
||||
setSelectedProgram,
|
||||
moveColumn,
|
||||
};
|
||||
}
|
||||
201
ui/src/storage/adapters/androidStorage.js
Normal file
201
ui/src/storage/adapters/androidStorage.js
Normal file
@ -0,0 +1,201 @@
|
||||
import { IStorage } from "./implement";
|
||||
|
||||
export class AndroidStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.memoryCache = new Map(); // 内存缓存
|
||||
}
|
||||
|
||||
// 检查 Android 接口是否可用
|
||||
isAvailable() {
|
||||
return typeof window.AndroidAsset !== "undefined";
|
||||
}
|
||||
|
||||
// 调用原生接口
|
||||
async callNative(method, ...args) {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error("AndroidAsset not available");
|
||||
}
|
||||
return window.AndroidAsset[method](...args);
|
||||
}
|
||||
|
||||
// 基础操作 (使用 localStorage 作为后备)
|
||||
async get(key) {
|
||||
try {
|
||||
const value = await this.callNative("getItem", key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return this.memoryCache.get(key) || null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
this.memoryCache.set(key, value);
|
||||
try {
|
||||
await this.callNative("setItem", key, JSON.stringify(value));
|
||||
} catch {
|
||||
// 忽略原生存储失败
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
this.memoryCache.delete(key);
|
||||
try {
|
||||
await this.callNative("removeItem", key);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
await this.callNative("clear");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 频道数据 (使用专用接口)
|
||||
async getChannels() {
|
||||
try {
|
||||
const data = await this.callNative("readChannelData");
|
||||
if (data && !data.startsWith("ERROR:")) {
|
||||
// 解析频道数据并缓存
|
||||
const channels = this.parseChannelData(data);
|
||||
this.memoryCache.set("channels", channels);
|
||||
return channels;
|
||||
}
|
||||
} catch {}
|
||||
return this.memoryCache.get("channels") || [];
|
||||
}
|
||||
|
||||
async setChannels(channels) {
|
||||
this.memoryCache.set("channels", channels);
|
||||
// 原生端通过文件存储,这里只更新内存缓存
|
||||
await this.setCacheMeta(
|
||||
"channels",
|
||||
new CacheMeta({
|
||||
key: "channels",
|
||||
updatedAt: Date.now(),
|
||||
size: channels.length,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
parseChannelData(text) {
|
||||
// 简单的 TXT 格式解析
|
||||
const channels = [];
|
||||
const lines = text.split("\n");
|
||||
let currentGroup = "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.includes("#genre#")) {
|
||||
currentGroup = trimmed.split(",")[0];
|
||||
} else if (trimmed.includes(",")) {
|
||||
const [name, url] = trimmed.split(",").map((s) => s.trim());
|
||||
if (name && url) {
|
||||
const existing = channels.find(
|
||||
(c) => c.name === name && c.group === currentGroup,
|
||||
);
|
||||
if (existing) {
|
||||
existing.urls.push(url);
|
||||
} else {
|
||||
channels.push(
|
||||
new Channel({
|
||||
id: `${currentGroup}_${name}`,
|
||||
name,
|
||||
group: currentGroup,
|
||||
urls: [url],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
async getGroups() {
|
||||
const channels = await this.getChannels();
|
||||
const groups = new Set(channels.map((c) => c.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() {
|
||||
return (await this.get("subscriptions")) || [];
|
||||
}
|
||||
|
||||
async setSubscriptions(subs) {
|
||||
await this.set("subscriptions", subs);
|
||||
}
|
||||
|
||||
// 线路有效性 (使用 SharedPreferences)
|
||||
async getValidity(url) {
|
||||
const all = await this.getAllValidity();
|
||||
return all.find((v) => v.url === url);
|
||||
}
|
||||
|
||||
async setValidity(url, validity) {
|
||||
const all = await this.getAllValidity();
|
||||
const index = all.findIndex((v) => v.url === url);
|
||||
if (index >= 0) {
|
||||
all[index] = { ...validity, url };
|
||||
} else {
|
||||
all.push({ ...validity, url });
|
||||
}
|
||||
await this.set("validity", all);
|
||||
}
|
||||
|
||||
async getAllValidity() {
|
||||
return (await this.get("validity")) || [];
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() {
|
||||
return (await this.get("preferences")) || new Preferences();
|
||||
}
|
||||
|
||||
async setPreferences(prefs) {
|
||||
await this.set("preferences", prefs);
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) {
|
||||
const all = (await this.get("history")) || [];
|
||||
return all.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
async addHistory(item) {
|
||||
const all = (await this.get("history")) || [];
|
||||
all.push(item);
|
||||
// 保留最近 500 条
|
||||
if (all.length > 500) {
|
||||
all.splice(0, all.length - 500);
|
||||
}
|
||||
await this.set("history", all);
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
await this.remove("history");
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) {
|
||||
const all = (await this.get("cacheMeta")) || {};
|
||||
return all[key];
|
||||
}
|
||||
|
||||
async setCacheMeta(key, meta) {
|
||||
const all = (await this.get("cacheMeta")) || {};
|
||||
all[key] = { ...meta, key };
|
||||
await this.set("cacheMeta", all);
|
||||
}
|
||||
|
||||
async isCacheValid(key, ttl) {
|
||||
const meta = await this.getCacheMeta(key);
|
||||
if (!meta || !meta.updatedAt) return false;
|
||||
return Date.now() - meta.updatedAt < ttl;
|
||||
}
|
||||
}
|
||||
23
ui/src/storage/adapters/implement.js
Normal file
23
ui/src/storage/adapters/implement.js
Normal file
@ -0,0 +1,23 @@
|
||||
export class IStorage {
|
||||
// 基础操作
|
||||
async get(key) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
async set(key, value) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
async remove(key) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
async clear() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
// 频道数据
|
||||
async getGroups() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
async getChannels() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
65
ui/src/storage/adapters/indexDBStorage.js
Normal file
65
ui/src/storage/adapters/indexDBStorage.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { openDB } from "idb";
|
||||
import { IStorage } from "./implement";
|
||||
|
||||
const DB_NAME = "IPTVStorage";
|
||||
const DB_VERSION = 1;
|
||||
|
||||
export class IndexedDBStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.db) return;
|
||||
|
||||
this.db = await openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// 频道分组
|
||||
if (!db.objectStoreNames.contains("groups")) {
|
||||
db.createObjectStore("groups", {
|
||||
keyPath: "id",
|
||||
});
|
||||
}
|
||||
|
||||
// 频道列表
|
||||
if (!db.objectStoreNames.contains("channels")) {
|
||||
db.createObjectStore("channels", {
|
||||
keyPath: "id",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 基础操作
|
||||
async get(key) {
|
||||
await this.init();
|
||||
return await this.db.get("channels", key);
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
await this.init();
|
||||
await this.db.put("channels", value, key);
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
await this.init();
|
||||
await this.db.delete("channels", key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.init();
|
||||
const stores = ["groups", "channels"];
|
||||
for (const store of stores) {
|
||||
await this.db.clear(store);
|
||||
}
|
||||
}
|
||||
|
||||
// 频道数据
|
||||
async getChannels() {}
|
||||
|
||||
async setChannels(channels) {}
|
||||
|
||||
async getGroups() {}
|
||||
}
|
||||
14067
ui/src/storage/adapters/localStorage.data.js
Normal file
14067
ui/src/storage/adapters/localStorage.data.js
Normal file
File diff suppressed because it is too large
Load Diff
48
ui/src/storage/adapters/localStorage.js
Normal file
48
ui/src/storage/adapters/localStorage.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { IStorage } from "./implement";
|
||||
import { allData } from "./localStorage.data";
|
||||
|
||||
const groups = allData.reduce(
|
||||
(res, item) => {
|
||||
if (!res.obj[item.group]) {
|
||||
res.obj[item.group] = { group: item.group, id: item.group, count: 0 };
|
||||
res.list.push(res.obj[item.group]);
|
||||
} else {
|
||||
res.obj[item.group].count += 1;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
{ list: [], obj: {} },
|
||||
);
|
||||
|
||||
const channels = allData.reduce(
|
||||
(res, item) => {
|
||||
if (!res.obj[item.id]) {
|
||||
res.obj[item.id] = {
|
||||
id: item.id,
|
||||
name: item.id,
|
||||
groupId: item.group,
|
||||
total: 0,
|
||||
active: 0,
|
||||
urls: [],
|
||||
};
|
||||
res.list.push(res.obj[item.id]);
|
||||
}
|
||||
|
||||
if (!res.obj[item.id].urls.includes(item.url)) {
|
||||
res.obj[item.id].urls.push(item.url);
|
||||
res.obj[item.id].total += 1;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{ list: [], obj: {} },
|
||||
);
|
||||
|
||||
export class LocalStorage extends IStorage {
|
||||
getGroups() {
|
||||
return groups.list;
|
||||
}
|
||||
getChannels() {
|
||||
return channels.list;
|
||||
}
|
||||
}
|
||||
@ -1,467 +1,29 @@
|
||||
import { openDB } from 'idb';
|
||||
import {
|
||||
Subscription, Channel, SourceValidity,
|
||||
Preferences, PlayHistory, CacheMeta
|
||||
} from './types.js';
|
||||
|
||||
const DB_NAME = 'IPTVStorage';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
/**
|
||||
* 抽象存储接口
|
||||
*/
|
||||
class IStorage {
|
||||
// 基础操作
|
||||
async get(key) { throw new Error('Not implemented'); }
|
||||
async set(key, value) { throw new Error('Not implemented'); }
|
||||
async remove(key) { throw new Error('Not implemented'); }
|
||||
async clear() { throw new Error('Not implemented'); }
|
||||
|
||||
// 频道数据
|
||||
async getChannels() { throw new Error('Not implemented'); }
|
||||
async setChannels(channels) { throw new Error('Not implemented'); }
|
||||
async getGroups() { throw new Error('Not implemented'); }
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() { throw new Error('Not implemented'); }
|
||||
async setSubscriptions(subs) { throw new Error('Not implemented'); }
|
||||
|
||||
// 线路有效性
|
||||
async getValidity(url) { throw new Error('Not implemented'); }
|
||||
async setValidity(url, validity) { throw new Error('Not implemented'); }
|
||||
async getAllValidity() { throw new Error('Not implemented'); }
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() { throw new Error('Not implemented'); }
|
||||
async setPreferences(prefs) { throw new Error('Not implemented'); }
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) { throw new Error('Not implemented'); }
|
||||
async addHistory(item) { throw new Error('Not implemented'); }
|
||||
async clearHistory() { throw new Error('Not implemented'); }
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) { throw new Error('Not implemented'); }
|
||||
async setCacheMeta(key, meta) { throw new Error('Not implemented'); }
|
||||
async isCacheValid(key, ttl) { throw new Error('Not implemented'); }
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexedDB 实现 (Web/Desktop)
|
||||
*/
|
||||
class IndexedDBStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.db) return;
|
||||
|
||||
this.db = await openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// 存储对象定义
|
||||
if (!db.objectStoreNames.contains('channels')) {
|
||||
const channelStore = db.createObjectStore('channels', { keyPath: 'id' });
|
||||
channelStore.createIndex('group', 'group');
|
||||
channelStore.createIndex('name', 'name');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('subscriptions')) {
|
||||
db.createObjectStore('subscriptions', { keyPath: 'id' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('validity')) {
|
||||
db.createObjectStore('validity', { keyPath: 'url' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('preferences')) {
|
||||
db.createObjectStore('preferences');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('history')) {
|
||||
const historyStore = db.createObjectStore('history', {
|
||||
keyPath: 'timestamp',
|
||||
autoIncrement: true
|
||||
});
|
||||
historyStore.createIndex('channelId', 'channelId');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('cacheMeta')) {
|
||||
db.createObjectStore('cacheMeta', { keyPath: 'key' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('generic')) {
|
||||
db.createObjectStore('generic');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 基础操作
|
||||
async get(key) {
|
||||
await this.init();
|
||||
return await this.db.get('generic', key);
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
await this.init();
|
||||
await this.db.put('generic', value, key);
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
await this.init();
|
||||
await this.db.delete('generic', key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.init();
|
||||
const stores = ['channels', 'subscriptions', 'validity', 'history', 'cacheMeta', 'generic'];
|
||||
for (const store of stores) {
|
||||
await this.db.clear(store);
|
||||
}
|
||||
}
|
||||
|
||||
// 频道数据
|
||||
async getChannels() {
|
||||
await this.init();
|
||||
return await this.db.getAll('channels');
|
||||
}
|
||||
|
||||
async setChannels(channels) {
|
||||
await this.init();
|
||||
const tx = this.db.transaction('channels', 'readwrite');
|
||||
await tx.store.clear();
|
||||
for (const channel of channels) {
|
||||
await tx.store.put(channel);
|
||||
}
|
||||
await tx.done;
|
||||
|
||||
// 更新缓存元数据
|
||||
await this.setCacheMeta('channels', new CacheMeta({
|
||||
key: 'channels',
|
||||
updatedAt: Date.now(),
|
||||
size: channels.length
|
||||
}));
|
||||
}
|
||||
|
||||
async getGroups() {
|
||||
const channels = await this.getChannels();
|
||||
const groups = new Set(channels.map(c => c.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() {
|
||||
await this.init();
|
||||
return await this.db.getAll('subscriptions');
|
||||
}
|
||||
|
||||
async setSubscriptions(subs) {
|
||||
await this.init();
|
||||
const tx = this.db.transaction('subscriptions', 'readwrite');
|
||||
await tx.store.clear();
|
||||
for (const sub of subs) {
|
||||
await tx.store.put(sub);
|
||||
}
|
||||
await tx.done;
|
||||
}
|
||||
|
||||
// 线路有效性
|
||||
async getValidity(url) {
|
||||
await this.init();
|
||||
return await this.db.get('validity', url);
|
||||
}
|
||||
|
||||
async setValidity(url, validity) {
|
||||
await this.init();
|
||||
await this.db.put('validity', { ...validity, url });
|
||||
}
|
||||
|
||||
async getAllValidity() {
|
||||
await this.init();
|
||||
return await this.db.getAll('validity');
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() {
|
||||
await this.init();
|
||||
const prefs = await this.db.get('preferences', 'user');
|
||||
return prefs || new Preferences();
|
||||
}
|
||||
|
||||
async setPreferences(prefs) {
|
||||
await this.init();
|
||||
await this.db.put('preferences', prefs, 'user');
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) {
|
||||
await this.init();
|
||||
const all = await this.db.getAll('history');
|
||||
return all.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
async addHistory(item) {
|
||||
await this.init();
|
||||
await this.db.add('history', item);
|
||||
|
||||
// 清理旧记录(保留最近 500 条)
|
||||
const count = await this.db.count('history');
|
||||
if (count > 500) {
|
||||
const toDelete = count - 500;
|
||||
const keys = await this.db.getAllKeys('history', null, toDelete);
|
||||
for (const key of keys) {
|
||||
await this.db.delete('history', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
await this.init();
|
||||
await this.db.clear('history');
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) {
|
||||
await this.init();
|
||||
return await this.db.get('cacheMeta', key);
|
||||
}
|
||||
|
||||
async setCacheMeta(key, meta) {
|
||||
await this.init();
|
||||
await this.db.put('cacheMeta', { ...meta, key });
|
||||
}
|
||||
|
||||
async isCacheValid(key, ttl) {
|
||||
const meta = await this.getCacheMeta(key);
|
||||
if (!meta || !meta.updatedAt) return false;
|
||||
return Date.now() - meta.updatedAt < ttl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Native 存储实现 (Android/TV)
|
||||
* 通过 AndroidAsset 接口调用原生 SQLite/SharedPreferences
|
||||
*/
|
||||
class NativeStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.memoryCache = new Map(); // 内存缓存
|
||||
}
|
||||
|
||||
// 检查 Android 接口是否可用
|
||||
isAvailable() {
|
||||
return typeof window.AndroidAsset !== 'undefined';
|
||||
}
|
||||
|
||||
// 调用原生接口
|
||||
async callNative(method, ...args) {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error('AndroidAsset not available');
|
||||
}
|
||||
return window.AndroidAsset[method](...args);
|
||||
}
|
||||
|
||||
// 基础操作 (使用 localStorage 作为后备)
|
||||
async get(key) {
|
||||
try {
|
||||
const value = await this.callNative('getItem', key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return this.memoryCache.get(key) || null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
this.memoryCache.set(key, value);
|
||||
try {
|
||||
await this.callNative('setItem', key, JSON.stringify(value));
|
||||
} catch {
|
||||
// 忽略原生存储失败
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
this.memoryCache.delete(key);
|
||||
try {
|
||||
await this.callNative('removeItem', key);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
await this.callNative('clear');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 频道数据 (使用专用接口)
|
||||
async getChannels() {
|
||||
try {
|
||||
const data = await this.callNative('readChannelData');
|
||||
if (data && !data.startsWith('ERROR:')) {
|
||||
// 解析频道数据并缓存
|
||||
const channels = this.parseChannelData(data);
|
||||
this.memoryCache.set('channels', channels);
|
||||
return channels;
|
||||
}
|
||||
} catch {}
|
||||
return this.memoryCache.get('channels') || [];
|
||||
}
|
||||
|
||||
async setChannels(channels) {
|
||||
this.memoryCache.set('channels', channels);
|
||||
// 原生端通过文件存储,这里只更新内存缓存
|
||||
await this.setCacheMeta('channels', new CacheMeta({
|
||||
key: 'channels',
|
||||
updatedAt: Date.now(),
|
||||
size: channels.length
|
||||
}));
|
||||
}
|
||||
|
||||
parseChannelData(text) {
|
||||
// 简单的 TXT 格式解析
|
||||
const channels = [];
|
||||
const lines = text.split('\n');
|
||||
let currentGroup = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.includes('#genre#')) {
|
||||
currentGroup = trimmed.split(',')[0];
|
||||
} else if (trimmed.includes(',')) {
|
||||
const [name, url] = trimmed.split(',').map(s => s.trim());
|
||||
if (name && url) {
|
||||
const existing = channels.find(c => c.name === name && c.group === currentGroup);
|
||||
if (existing) {
|
||||
existing.urls.push(url);
|
||||
} else {
|
||||
channels.push(new Channel({
|
||||
id: `${currentGroup}_${name}`,
|
||||
name,
|
||||
group: currentGroup,
|
||||
urls: [url]
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
async getGroups() {
|
||||
const channels = await this.getChannels();
|
||||
const groups = new Set(channels.map(c => c.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() {
|
||||
return await this.get('subscriptions') || [];
|
||||
}
|
||||
|
||||
async setSubscriptions(subs) {
|
||||
await this.set('subscriptions', subs);
|
||||
}
|
||||
|
||||
// 线路有效性 (使用 SharedPreferences)
|
||||
async getValidity(url) {
|
||||
const all = await this.getAllValidity();
|
||||
return all.find(v => v.url === url);
|
||||
}
|
||||
|
||||
async setValidity(url, validity) {
|
||||
const all = await this.getAllValidity();
|
||||
const index = all.findIndex(v => v.url === url);
|
||||
if (index >= 0) {
|
||||
all[index] = { ...validity, url };
|
||||
} else {
|
||||
all.push({ ...validity, url });
|
||||
}
|
||||
await this.set('validity', all);
|
||||
}
|
||||
|
||||
async getAllValidity() {
|
||||
return await this.get('validity') || [];
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() {
|
||||
return await this.get('preferences') || new Preferences();
|
||||
}
|
||||
|
||||
async setPreferences(prefs) {
|
||||
await this.set('preferences', prefs);
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) {
|
||||
const all = await this.get('history') || [];
|
||||
return all.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
async addHistory(item) {
|
||||
const all = await this.get('history') || [];
|
||||
all.push(item);
|
||||
// 保留最近 500 条
|
||||
if (all.length > 500) {
|
||||
all.splice(0, all.length - 500);
|
||||
}
|
||||
await this.set('history', all);
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
await this.remove('history');
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) {
|
||||
const all = await this.get('cacheMeta') || {};
|
||||
return all[key];
|
||||
}
|
||||
|
||||
async setCacheMeta(key, meta) {
|
||||
const all = await this.get('cacheMeta') || {};
|
||||
all[key] = { ...meta, key };
|
||||
await this.set('cacheMeta', all);
|
||||
}
|
||||
|
||||
async isCacheValid(key, ttl) {
|
||||
const meta = await this.getCacheMeta(key);
|
||||
if (!meta || !meta.updatedAt) return false;
|
||||
return Date.now() - meta.updatedAt < ttl;
|
||||
}
|
||||
}
|
||||
import { IndexedDBStorage } from "./adapters/indexDBStorage";
|
||||
import { AndroidStorage } from "./adapters/androidStorage";
|
||||
import { LocalStorage } from "./adapters/localStorage";
|
||||
|
||||
/**
|
||||
* 存储工厂 - 根据平台创建对应的存储实例
|
||||
*/
|
||||
export function createStorage() {
|
||||
const platform = import.meta.env.VITE_PLATFORM || 'web';
|
||||
let storage;
|
||||
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||
const mode = import.meta.env.MODE || "development";
|
||||
|
||||
if (platform === 'android' || platform === 'tv') {
|
||||
return new NativeStorage();
|
||||
if (mode === "development") {
|
||||
storage = new LocalStorage();
|
||||
} else {
|
||||
if ([].includes(platform)) {
|
||||
storage = new IndexedDBStorage();
|
||||
} else if (["android", "tv"].includes(platform)) {
|
||||
storage = new AndroidStorage();
|
||||
} else {
|
||||
storage = new IndexedDBStorage();
|
||||
}
|
||||
}
|
||||
|
||||
return new IndexedDBStorage();
|
||||
return storage;
|
||||
}
|
||||
|
||||
// 导出类型和类
|
||||
export {
|
||||
IStorage,
|
||||
IndexedDBStorage,
|
||||
NativeStorage,
|
||||
Subscription,
|
||||
Channel,
|
||||
SourceValidity,
|
||||
Preferences,
|
||||
PlayHistory,
|
||||
CacheMeta
|
||||
};
|
||||
|
||||
// 默认导出
|
||||
export default createStorage;
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
/**
|
||||
* 存储类型定义
|
||||
*/
|
||||
|
||||
// 订阅源
|
||||
export class Subscription {
|
||||
constructor(data = {}) {
|
||||
this.id = data.id || crypto.randomUUID();
|
||||
this.name = data.name || '';
|
||||
this.url = data.url || '';
|
||||
this.type = data.type || 'm3u'; // 'm3u' | 'txt'
|
||||
this.enabled = data.enabled !== false;
|
||||
this.lastUpdated = data.lastUpdated || 0;
|
||||
this.etag = data.etag || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 频道
|
||||
export class Channel {
|
||||
constructor(data = {}) {
|
||||
this.id = data.id || '';
|
||||
this.name = data.name || '';
|
||||
this.group = data.group || '';
|
||||
this.urls = data.urls || []; // string[]
|
||||
this.logo = data.logo || '';
|
||||
this.epgId = data.epgId || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 线路有效性
|
||||
export class SourceValidity {
|
||||
constructor(data = {}) {
|
||||
this.url = data.url || '';
|
||||
this.status = data.status || 'unknown'; // 'online' | 'offline' | 'unknown'
|
||||
this.checkedAt = data.checkedAt || 0;
|
||||
this.latency = data.latency || 0;
|
||||
this.failCount = data.failCount || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
export class Preferences {
|
||||
constructor(data = {}) {
|
||||
this.defaultGroup = data.defaultGroup || '';
|
||||
this.autoPlay = data.autoPlay !== false;
|
||||
this.preferredQuality = data.preferredQuality || 'auto'; // 'auto' | 'hd' | 'sd'
|
||||
this.volume = data.volume ?? 1.0;
|
||||
this.listCacheTTL = data.listCacheTTL || 24 * 60 * 60 * 1000; // 1天
|
||||
this.validityCacheTTL = data.validityCacheTTL || 12 * 60 * 60 * 1000; // 12小时
|
||||
}
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
export class PlayHistory {
|
||||
constructor(data = {}) {
|
||||
this.channelId = data.channelId || '';
|
||||
this.channelName = data.channelName || '';
|
||||
this.sourceUrl = data.sourceUrl || '';
|
||||
this.timestamp = data.timestamp || Date.now();
|
||||
this.duration = data.duration || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
export class CacheMeta {
|
||||
constructor(data = {}) {
|
||||
this.key = data.key || '';
|
||||
this.updatedAt = data.updatedAt || 0;
|
||||
this.size = data.size || 0;
|
||||
this.version = data.version || 1;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user