feat: ui改版

This commit is contained in:
liyanyan 2026-02-11 00:20:00 +08:00 committed by 李岩岩
parent c0428c5d3d
commit 7edb8e4d93
29 changed files with 14627 additions and 3003 deletions

993
ui/package-lock.json generated
View File

@ -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"
}
}
}
}

View File

@ -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
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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>

View 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,
};
};

View File

@ -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
}
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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
}
}

View 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,
};
};

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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;
}

View 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,
};
};

View File

@ -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,
};
}

View 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;
}
}

View 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");
}
}

View 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() {}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@ -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;

View File

@ -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;
}
}