Compare commits

...

7 Commits

Author SHA1 Message Date
李岩岩
c8bb8537c8 chore(config): 更新 gitignore 和 README
- 添加 Docker、Android、Web 相关忽略项
- 更新 README 项目结构和构建说明
- 添加 Android TV 文档
2026-02-05 12:42:12 +08:00
李岩岩
4f47258b17 chore(build): 更新 Windows 构建脚本
- 使用 npm run build 替代 build:web
- 修复路径问题
2026-02-05 12:42:04 +08:00
李岩岩
7bef512b07 chore(desktop): 更新 Tauri 配置
- 修改 distDir 指向 ui/dist-web
- 适配新的构建输出目录结构
2026-02-05 12:41:57 +08:00
李岩岩
52fc8099ae feat(web): 创建 Docker Web 服务端
- 添加 Express 服务端,提供频道 API
- 添加 M3U8/TS 流代理,解决跨域问题
- 添加 Dockerfile 和 docker-compose.yml
- 添加 Nginx 反向代理配置
- 支持多阶段构建,自动打包前端
2026-02-05 12:41:50 +08:00
李岩岩
2a565fb8da feat(android-tv): 创建 Android TV WebView 壳应用
- 添加 TV 专用的 Leanback 主题和配置
- 支持遥控器 D-Pad 导航
- 添加 AssetReader JS 接口
- 强制横屏显示
- 处理遥控器按键(返回、菜单、信息)
2026-02-05 12:41:41 +08:00
李岩岩
327f03c562 feat(android): 创建 Android WebView 壳应用
- 添加 MainActivity 和 WebView 配置
- 添加 AssetReader JS 接口用于读取本地文件
- 支持全屏、横屏模式
- 添加错误页面和测试页面
- 添加 Gradle 构建配置和一键构建脚本
2026-02-05 12:41:33 +08:00
李岩岩
b7f16e1444 feat(ui): 添加调试面板和 Android 本地文件加载支持
- 添加调试信息面板,显示加载状态和错误信息
- 支持 Android WebView 本地文件读取 (AndroidAsset)
- 优化加载流程,支持网络/本地/模拟数据多种模式
- 修复 vite.config.js 输出目录配置
2026-02-05 12:41:24 +08:00
56 changed files with 5304 additions and 165 deletions

129
.gitignore vendored
View File

@ -1,6 +1,125 @@
# ================================================
# System files
# ================================================
.DS_Store
dist-web
node_modules
package-lock.json
pnpm-lock.yaml
desktop/src-tauri/target
Thumbs.db
# ================================================
# IDE
# ================================================
.idea/
.vscode/settings.json
*.swp
*.swo
*~
# ================================================
# Dependencies
# ================================================
node_modules/
.pnpm-store/
# ================================================
# Build outputs
# ================================================
# UI
ui/dist-web/
ui/dist/
# Web
web/public/
web/dist/
# Desktop
desktop/src-tauri/target/
desktop/dist/
desktop/*.log
# ================================================
# Android
# ================================================
android/.gradle/
android/.idea/
android/local.properties
android/*.iml
android/app/build/
android/app/release/
android/captures/
android/*.hprof
android/.cxx/
android/app/src/main/assets/www/
!android/app/src/main/assets/error.html
!android/app/src/main/assets/test.html
# ================================================
# Android TV
# ================================================
android-tv/.gradle/
android-tv/.idea/
android-tv/local.properties
android-tv/*.iml
android-tv/app/build/
android-tv/app/release/
android-tv/captures/
android-tv/*.hprof
android-tv/.cxx/
android-tv/app/src/main/assets/www/
!android-tv/app/src/main/assets/error.html
!android-tv/app/src/main/assets/test.html
# ================================================
# HarmonyOS
# ================================================
harmonyos/.idea/
harmonyos/build/
harmonyos/entry/build/
# ================================================
# Docker
# ================================================
docker-compose.override.yml
.env.docker
.env.production
*.pem
*.key
# ================================================
# Logs
# ================================================
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# ================================================
# Environment
# ================================================
.env
.env.local
.env.*.local
.env.development
.env.test
# ================================================
# Testing
# ================================================
coverage/
.nyc_output/
# ================================================
# Temporary files
# ================================================
*.tmp
*.temp
.cache/
*.pid
*.seed
*.pid.lock
# ================================================
# Misc
# ================================================
*.tgz
*.tar.gz

71
README.md Normal file
View File

@ -0,0 +1,71 @@
# 📺 IPTV 跨平台应用
基于 Web 技术的跨平台 IPTV 播放器,一套代码支持 Windows、Mac、Android、Android TV、HarmonyOS。
## 项目结构
```
iptv-app/
├── ui/ # 🌐 Web 核心代码 (Vue 3)
│ ├── src/
│ │ ├── components/ # 播放器组件
│ │ ├── utils/ # M3U 解析等工具
│ │ └── App.vue # 主应用
│ ├── public/ # 静态资源
│ └── dist-web/ # 📦 Web 构建输出(各平台共用)
├── web/ # 网页端nodejs中间层+docker封装
├── desktop/ # 🖥️ Tauri 桌面端 (Win/Mac)
├── android/ # 📱 Android WebView 壳
├── android-tv/ # 📺 Android TV WebView 壳
└── harmonyos/ # 🔶 HarmonyOS WebView 壳
```
## 技术栈
| 平台 | 技术 | 说明 |
|------|------|------|
| UI 核心 | Vue 3 + Vite + hls.js | 一套代码,所有平台共用 |
| Web | Nodejs | 中间层代理,解决跨域问题 |
| Desktop | Tauri (Rust) | 轻量 (~5MB),高性能 |
| Android | WebView + ExoPlayer | 手机/平板 Web 壳 |
| Android TV | WebView + Leanback | TV 专用,支持遥控器导航 |
| HarmonyOS | Web 组件 + Video 组件 | Web 壳 + 原生播放器 |
## 快速开始
### Docker Web推荐
```bash
# 1. 构建 Web UI
cd ui && npm run build
# 2. 启动 Docker 服务
cd ../web
docker-compose up -d
# 访问 http://localhost:3000
```
### Android 构建
```bash
# 1. 构建 Web UI
cd ui
npm install
npm run build
# 2. 构建 Android APK
cd ../android
./build.sh
```
APK 输出位置: `android/app/build/outputs/apk/debug/app-debug.apk`
### Android TV 构建
```bash
cd android-tv
./build.sh
```
---

101
android-tv/README.md Normal file
View File

@ -0,0 +1,101 @@
# Android TV WebView 壳
为 Android TV 设备(包括智能电视、电视盒子等)提供的 WebView 套壳应用。
## 特性
- ✅ **Leanback 支持** - 专为 TV 优化的主题和启动器
- ✅ **D-Pad 导航** - 遥控器方向键支持,自动注入 JS 导航
- ✅ **全屏沉浸** - 无状态栏/导航栏,纯全屏体验
- ✅ **横屏锁定** - 强制横屏显示
- ✅ **按键处理** - 支持返回、菜单、信息键等遥控器按键
## 目录结构
```
android-tv/
├── app/src/main/
│ ├── java/com/iptv/tv/
│ │ └── MainActivity.java # TV 主活动(含 D-Pad 支持)
│ ├── res/
│ │ ├── layout/activity_main.xml
│ │ ├── values/
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml # Leanback 主题
│ │ │ └── colors.xml
│ │ └── drawable/
│ │ └── ic_banner.xml # TV 启动器横幅
│ ├── assets/
│ │ └── www/ # 打包的 Web 资源
│ └── AndroidManifest.xml # TV 特定配置
├── build.sh # 一键构建脚本
└── ...
```
## 构建步骤
```bash
# 1. 构建 Web UI
cd ../ui
npm install
npm run build
# 2. 构建 Android TV APK
cd ../android-tv
./build.sh
```
APK 输出: `app/build/outputs/apk/debug/app-debug.apk`
## TV 特定配置
### AndroidManifest.xml
```xml
<!-- TV 必需特性 -->
<uses-feature android:name="android.software.leanback" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<!-- TV 启动器 -->
<intent-filter>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
```
### D-Pad 导航支持
应用自动注入 JavaScript 代码,使 Web 应用支持遥控器方向键导航:
- **方向键** - 在可聚焦元素间移动
- **确认键** - 点击当前聚焦元素
- **返回键** - 页面后退/退出应用
- **菜单键** - 刷新页面
- **信息键** - 显示版本信息
## 遥控器按键映射
| 按键 | 功能 |
|------|------|
| 上/下/左/右 | 导航焦点 |
| 确认/OK | 点击 |
| 返回 | 页面后退 |
| 菜单 | 刷新页面 |
| 信息/INFO | 显示版本 |
## 开发与调试
1. **ADB 连接 TV**
```bash
adb connect <TV_IP>:5555
adb install app-debug.apk
```
2. **查看日志**
```bash
adb logcat -s IPTV:D
```
3. **远程调试**
```bash
adb shell am start -a android.intent.action.VIEW -d "http://<dev_server>:5173"
```

View File

@ -0,0 +1,33 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.iptv.tv'
compileSdk 34
defaultConfig {
applicationId "com.iptv.tv"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}

1
android-tv/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1 @@
# ProGuard rules

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.iptv.tv">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- TV 特定配置 -->
<uses-feature android:name="android.software.leanback" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_banner"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:banner="@drawable/ic_banner">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize"
android:screenOrientation="landscape"
android:theme="@style/AppTheme.NoActionBar">
<!-- TV Launcher -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- 普通 Launcher用于调试 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,48 @@
package com.iptv.tv;
import android.content.Context;
import android.webkit.JavascriptInterface;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class AssetReader {
private Context context;
public AssetReader(Context context) {
this.context = context;
}
@JavascriptInterface
public String readFile(String path) {
try {
InputStream is = context.getAssets().open(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
} catch (IOException e) {
return "ERROR: " + e.getMessage();
}
}
@JavascriptInterface
public String readChannelData() {
return readFile("www/api/result.txt");
}
@JavascriptInterface
public boolean fileExists(String path) {
try {
context.getAssets().open(path).close();
return true;
} catch (IOException e) {
return false;
}
}
}

View File

@ -0,0 +1,148 @@
package com.iptv.tv;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class MainActivity extends Activity {
private static final String TAG = "IPTV_TV";
private WebView webView;
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate started");
// 全屏设置
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
// 设置内容视图
webView = new WebView(this);
webView.setBackgroundColor(Color.BLACK);
setContentView(webView);
// 添加 JavaScript 接口
webView.addJavascriptInterface(new AssetReader(this), "AndroidAsset");
// WebView 设置
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setMediaPlaybackRequiresUserGesture(false);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
// TV 不需要缩放
settings.setSupportZoom(false);
settings.setBuiltInZoomControls(false);
// WebViewClient
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
Log.d(TAG, "onPageFinished: " + url);
injectDPadSupport();
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.e(TAG, "onReceivedError: " + errorCode + " - " + description);
}
});
// WebChromeClient
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d(TAG, "Console: " + consoleMessage.message());
return true;
}
});
// 延迟加载
new Handler(Looper.getMainLooper()).postDelayed(() -> {
webView.loadUrl("file:///android_asset/www/index.html");
}, 100);
Log.d(TAG, "onCreate finished");
}
private void injectDPadSupport() {
String js = "javascript:(function() {" +
"document.addEventListener('keydown', function(e) {" +
" if(e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {" +
" e.preventDefault();" +
" var focusable = document.querySelectorAll('button, a, input, [tabindex]:not([tabindex=\"-1\"])');" +
" var current = document.activeElement;" +
" var index = Array.prototype.indexOf.call(focusable, current);" +
" if(e.key === 'ArrowRight' || e.key === 'ArrowDown') {" +
" var next = focusable[index + 1] || focusable[0];" +
" next.focus();" +
" } else {" +
" var prev = focusable[index - 1] || focusable[focusable.length - 1];" +
" prev.focus();" +
" }" +
" }" +
" if(e.key === 'Enter') {" +
" document.activeElement.click();" +
" }" +
"});" +
"})()";
webView.evaluateJavascript(js, null);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
webView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onPause() {
super.onPause();
if (webView != null) webView.onPause();
}
@Override
protected void onResume() {
super.onResume();
if (webView != null) webView.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (webView != null) webView.destroy();
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#0a0a0a" />
</shape>
</item>
<item
android:left="48dp"
android:right="48dp"
android:top="20dp"
android:bottom="20dp"
android:gravity="center">
<vector
android:width="160dp"
android:height="60dp"
android:viewportWidth="160"
android:viewportHeight="60">
<path
android:fillColor="#FFFFFF"
android:pathData="M15,10 L15,50 L23,50 L23,34 L40,50 L52,50 L52,10 L40,10 L23,26 L23,10 Z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M65,20 L65,25 L70,25 L70,35 L65,35 L65,40 L85,40 L85,35 L90,35 L90,25 L85,25 L85,20 Z M75,30 L80,30 L80,32 L75,32 Z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M95,20 L95,40 L105,40 L105,35 L115,35 L115,40 L125,40 L125,20 L115,20 L115,25 L105,25 L105,20 Z M105,30 L115,30 L115,32 L105,32 Z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M130,20 L130,40 L150,40 L150,32 L140,32 L140,20 Z"/>
</vector>
</item>
</layer-list>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0a0a0a">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0a0a0a"
android:focusable="true"
android:focusableInTouchMode="true" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">IPTV TV</string>
</resources>

View File

@ -0,0 +1,12 @@
<resources>
<style name="AppTheme" parent="Theme.Leanback">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
<style name="AppTheme.NoActionBar" parent="AppTheme">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

4
android-tv/build.gradle Normal file
View File

@ -0,0 +1,4 @@
// Top-level build file
plugins {
id 'com.android.application' version '8.1.0' apply false
}

31
android-tv/build.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Android TV WebView 壳打包脚本
set -e
echo "=== IPTV Android TV 构建 ==="
# 检查 UI 构建产物
if [ ! -d "../ui/dist-web" ]; then
echo "错误: 未找到 ../ui/dist-web 目录"
echo "请先构建 Web UI: cd ../ui && npm run build"
exit 1
fi
# 复制 Web 资源到 Android assets
echo "复制 Web 资源..."
mkdir -p app/src/main/assets/www
cp -r ../ui/dist-web/* app/src/main/assets/www/
# 统计文件
echo "已复制文件数量:"
find app/src/main/assets/www -type f | wc -l
# 构建 Debug APK
echo "构建 Debug APK..."
./gradlew assembleDebug
echo ""
echo "=== 构建完成 ==="
echo "APK 位置: app/build/outputs/apk/debug/app-debug.apk"

View File

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
android.suppressUnsupportedCompileSdk=34

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

17
android-tv/gradlew vendored Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Gradle Wrapper Startup Script
# Find the script's directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WRAPPER_JAR="$SCRIPT_DIR/gradle/wrapper/gradle-wrapper.jar"
# Find Java
if [ -n "$JAVA_HOME" ] ; then
JAVA_CMD="$JAVA_HOME/bin/java"
else
JAVA_CMD="java"
fi
# Run Gradle wrapper
exec "$JAVA_CMD" -cp "$WRAPPER_JAR" org.gradle.wrapper.GradleWrapperMain "$@"

View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "IPTV TV"
include ':app'

71
android/README.md Normal file
View File

@ -0,0 +1,71 @@
# Android WebView 壳
## 目录结构
```
android/
├── app/src/main/
│ ├── java/com/iptv/app/
│ │ └── MainActivity.java # WebView 主活动
│ ├── res/
│ │ ├── layout/activity_main.xml
│ │ ├── values/
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ └── colors.xml
│ │ └── mipmap-*/ # 图标资源
│ ├── assets/
│ │ └── www/ # 打包的 Web 资源 (从 ui/dist-web 复制)
│ └── AndroidManifest.xml
├── build.gradle # 项目级构建配置
├── settings.gradle
└── gradle.properties
```
## 构建步骤
### 1. 构建 Web UI
```bash
cd ../ui
npm install
npm run build:web
```
### 2. 复制资源到 Android
```bash
# 将构建好的 web 资源复制到 Android assets
cp -r ../ui/dist-web/* app/src/main/assets/www/
```
### 3. 构建 APK
```bash
./gradlew assembleDebug
```
APK 输出位置: `app/build/outputs/apk/debug/app-debug.apk`
## 开发模式
如需连接开发服务器测试,修改 `MainActivity.java`:
```java
private static final String LOAD_MODE = "remote";
private static final String REMOTE_URL = "http://你的IP:5173";
```
## 功能特性
- ✅ WebView 加载 Web UI
- ✅ 全屏无标题栏
- ✅ 下拉刷新
- ✅ 返回键支持页面后退
- ✅ 视频全屏自动横屏
- ✅ 暗色主题
## 权限
- `INTERNET` - 网络访问
- `ACCESS_NETWORK_STATE` - 网络状态检测

33
android/app/build.gradle Normal file
View File

@ -0,0 +1,33 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.iptv.app'
compileSdk 34
defaultConfig {
applicationId "com.iptv.app"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}

1
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1 @@
# ProGuard rules

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.iptv.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载错误</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 20px;
text-align: center;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 12px;
color: #ff6b6b;
}
p {
font-size: 14px;
color: #888;
margin-bottom: 8px;
line-height: 1.6;
}
.tips {
margin-top: 30px;
padding: 16px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
max-width: 400px;
}
.tips h2 {
font-size: 14px;
margin-bottom: 10px;
color: #fff;
}
.tips ul {
text-align: left;
padding-left: 20px;
}
.tips li {
font-size: 13px;
color: #aaa;
margin-bottom: 6px;
}
</style>
</head>
<body>
<div class="icon">⚠️</div>
<h1>页面加载失败</h1>
<p>无法加载应用资源,可能是以下原因:</p>
<div class="tips">
<h2>可能的解决方案:</h2>
<ul>
<li>检查网络连接是否正常</li>
<li>清除应用数据后重试</li>
<li>重新安装应用</li>
<li>如果是开发模式,请检查远程服务器地址</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,49 @@
package com.iptv.app;
import android.content.Context;
import android.webkit.JavascriptInterface;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class AssetReader {
private Context context;
public AssetReader(Context context) {
this.context = context;
}
@JavascriptInterface
public String readFile(String path) {
try {
// path : "www/api/result.txt"
InputStream is = context.getAssets().open(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
} catch (IOException e) {
return "ERROR: " + e.getMessage();
}
}
@JavascriptInterface
public String readChannelData() {
return readFile("www/api/result.txt");
}
@JavascriptInterface
public boolean fileExists(String path) {
try {
context.getAssets().open(path).close();
return true;
} catch (IOException e) {
return false;
}
}
}

View File

@ -0,0 +1,231 @@
package com.iptv.app;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "IPTV";
private WebView webView;
private SwipeRefreshLayout swipeRefresh;
// 加载模式"local" 使用本地打包的web资源"remote" 使用远程服务器"test" 使用测试页面
private static final String LOAD_MODE = "local";
// 远程服务器地址开发测试用
private static final String REMOTE_URL = "http://192.168.1.100:5173";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate started");
// 必须在 setContentView 之前调用
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webview);
swipeRefresh = findViewById(R.id.swipe_refresh);
if (webView == null) {
Log.e(TAG, "WebView is null!");
return;
}
// 全屏设置 setContentView 之后
setFullscreen();
// 添加 JavaScript 接口用于读取本地文件
webView.addJavascriptInterface(new AssetReader(this), "AndroidAsset");
// WebView 设置
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setMediaPlaybackRequiresUserGesture(false);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
// 支持缩放
settings.setSupportZoom(true);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
// 启用硬件加速
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
// WebViewClient 处理页面跳转
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Log.d(TAG, "shouldOverrideUrlLoading: " + request.getUrl());
return false; // WebView 内打开链接
}
@Override
public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
Log.d(TAG, "onPageStarted: " + url);
}
@Override
public void onPageFinished(WebView view, String url) {
Log.d(TAG, "onPageFinished: " + url);
if (swipeRefresh != null) {
swipeRefresh.setRefreshing(false);
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.e(TAG, "onReceivedError: " + errorCode + " - " + description + " - " + failingUrl);
}
});
// WebChromeClient 处理全屏视频和 JS 控制台
webView.setWebChromeClient(new WebChromeClient() {
private View customView;
private CustomViewCallback customViewCallback;
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d(TAG, "WebView Console: " + consoleMessage.message() + " at " + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber());
return true;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
Log.d(TAG, "Loading progress: " + newProgress + "%");
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
customView = view;
customViewCallback = callback;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
@Override
public void onHideCustomView() {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
if (customViewCallback != null) {
customViewCallback.onCustomViewHidden();
}
customView = null;
}
});
// 下拉刷新
if (swipeRefresh != null) {
swipeRefresh.setOnRefreshListener(() -> webView.reload());
swipeRefresh.setColorSchemeResources(android.R.color.holo_blue_bright);
}
// 延迟加载页面确保 WebView 完全初始化
new Handler(Looper.getMainLooper()).postDelayed(() -> {
loadContent();
}, 100);
Log.d(TAG, "onCreate finished");
}
private void loadContent() {
Log.d(TAG, "loadContent, mode: " + LOAD_MODE);
String url;
if ("test".equals(LOAD_MODE)) {
// 加载测试页面红色背景
url = "file:///android_asset/test.html";
} else if ("local".equals(LOAD_MODE)) {
// 加载本地打包的web资源
url = "file:///android_asset/www/index.html";
} else {
// 加载远程服务器开发测试
url = REMOTE_URL;
}
Log.d(TAG, "Loading URL: " + url);
webView.loadUrl(url);
}
private void setFullscreen() {
// 设置状态栏和导航栏颜色
getWindow().setStatusBarColor(Color.BLACK);
getWindow().setNavigationBarColor(Color.BLACK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().setDecorFitsSystemWindows(false);
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
);
}
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
if (webView != null) {
webView.onResume();
}
setFullscreen();
}
@Override
protected void onPause() {
super.onPause();
if (webView != null) {
webView.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (webView != null) {
webView.destroy();
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FFFFFF"
android:pathData="M38,32 L38,76 L46,76 L46,58 L62,76 L74,76 L74,32 L62,32 L46,50 L46,32 Z"/>
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0a0a0a">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0a0a0a" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0a0a0a</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">IPTV</string>
</resources>

View File

@ -0,0 +1,9 @@
<resources>
<style name="AppTheme" parent="Theme.Material3.Dark.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>

4
android/build.gradle Normal file
View File

@ -0,0 +1,4 @@
// Top-level build file
plugins {
id 'com.android.application' version '8.1.0' apply false
}

31
android/build.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Android WebView 壳打包脚本
set -e
echo "=== IPTV Android 构建 ==="
# 检查 UI 构建产物
if [ ! -d "../ui/dist-web" ]; then
echo "错误: 未找到 ../ui/dist-web 目录"
echo "请先构建 Web UI: cd ../ui && npm run build"
exit 1
fi
# 复制 Web 资源到 Android assets
echo "复制 Web 资源..."
mkdir -p app/src/main/assets/www
cp -r ../ui/dist-web/* app/src/main/assets/www/
# 统计文件
echo "已复制文件数量:"
find app/src/main/assets/www -type f | wc -l
# 构建 Debug APK
echo "构建 Debug APK..."
./gradlew assembleDebug
echo ""
echo "=== 构建完成 ==="
echo "APK 位置: app/build/outputs/apk/debug/app-debug.apk"

View File

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

17
android/gradlew vendored Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Gradle Wrapper Startup Script
# Find the script's directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WRAPPER_JAR="$SCRIPT_DIR/gradle/wrapper/gradle-wrapper.jar"
# Find Java
if [ -n "$JAVA_HOME" ] ; then
JAVA_CMD="$JAVA_HOME/bin/java"
else
JAVA_CMD="java"
fi
# Run Gradle wrapper
exec "$JAVA_CMD" -cp "$WRAPPER_JAR" org.gradle.wrapper.GradleWrapperMain "$@"

16
android/settings.gradle Normal file
View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "IPTV App"
include ':app'

View File

@ -38,7 +38,6 @@ if (Test-Path "dist-web/api") {
Remove-Item "dist-web/api" -Recurse -Force
}
Copy-Item -Path "public/api" -Destination "dist-web/api" -Recurse -Force
Set-Location ..
# 步骤2: 安装依赖
Write-Host "📦 步骤 2/4: 安装 Tauri 依赖..." -ForegroundColor Cyan

315
desktop/package-lock.json generated Normal file
View File

@ -0,0 +1,315 @@
{
"name": "iptv-desktop",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "iptv-desktop",
"version": "1.0.0",
"devDependencies": {
"@tauri-apps/cli": "^1.6.3"
}
},
"node_modules/@tauri-apps/cli": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"dependencies": {
"semver": ">=7.5.2"
},
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
}
},
"dependencies": {
"@tauri-apps/cli": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"requires": {
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3",
"semver": ">=7.5.2"
}
},
"@tauri-apps/cli-darwin-arm64": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-darwin-x64": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm64-gnu": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm64-musl": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-x64-gnu": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-x64-musl": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-arm64-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-ia32-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-x64-msvc": {
"version": "1.6.3",
"resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"dev": true,
"optional": true
},
"semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true
}
}
}

View File

@ -2,8 +2,8 @@
"build": {
"beforeBuildCommand": "",
"beforeDevCommand": "",
"devPath": "../dist-web",
"distDir": "../dist-web",
"devPath": "../../ui/dist-web",
"distDir": "../../ui/dist-web",
"withGlobalTauri": false
},
"tauri": {

982
ui/package-lock.json generated Normal file
View File

@ -0,0 +1,982 @@
{
"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",
"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/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=="
},
"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"
}
}
}
}

786
ui/pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,786 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hls.js:
specifier: ^1.5.0
version: 1.6.15
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
version: 5.2.4(vite@5.4.21)(vue@3.5.27)
vite:
specifier: ^5.0.0
version: 5.4.21
packages:
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.0':
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/rollup-android-arm-eabi@4.57.1':
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.57.1':
resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.57.1':
resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.57.1':
resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.57.1':
resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.57.1':
resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.57.1':
resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.57.1':
resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.57.1':
resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.57.1':
resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.57.1':
resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@vitejs/plugin-vue@5.2.4':
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue/compiler-core@3.5.27':
resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==}
'@vue/compiler-dom@3.5.27':
resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==}
'@vue/compiler-sfc@3.5.27':
resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==}
'@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==}
'@vue/runtime-core@3.5.27':
resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==}
'@vue/runtime-dom@3.5.27':
resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==}
'@vue/server-renderer@3.5.27':
resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==}
peerDependencies:
vue: 3.5.27
'@vue/shared@3.5.27':
resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
hasBin: true
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hls.js@1.6.15:
resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.57.1:
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
vite@5.4.21:
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
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
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:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/parser@7.29.0':
dependencies:
'@babel/types': 7.29.0
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/rollup-android-arm-eabi@4.57.1':
optional: true
'@rollup/rollup-android-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-x64@4.57.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.57.1':
optional: true
'@rollup/rollup-freebsd-x64@4.57.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.57.1':
optional: true
'@rollup/rollup-openbsd-x64@4.57.1':
optional: true
'@rollup/rollup-openharmony-arm64@4.57.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.57.1':
optional: true
'@types/estree@1.0.8': {}
'@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.27)':
dependencies:
vite: 5.4.21
vue: 3.5.27
'@vue/compiler-core@3.5.27':
dependencies:
'@babel/parser': 7.29.0
'@vue/shared': 3.5.27
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.27':
dependencies:
'@vue/compiler-core': 3.5.27
'@vue/shared': 3.5.27
'@vue/compiler-sfc@3.5.27':
dependencies:
'@babel/parser': 7.29.0
'@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@3.5.27':
dependencies:
'@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
'@vue/runtime-core@3.5.27':
dependencies:
'@vue/reactivity': 3.5.27
'@vue/shared': 3.5.27
'@vue/runtime-dom@3.5.27':
dependencies:
'@vue/reactivity': 3.5.27
'@vue/runtime-core': 3.5.27
'@vue/shared': 3.5.27
csstype: 3.2.3
'@vue/server-renderer@3.5.27(vue@3.5.27)':
dependencies:
'@vue/compiler-ssr': 3.5.27
'@vue/shared': 3.5.27
vue: 3.5.27
'@vue/shared@3.5.27': {}
csstype@3.2.3: {}
entities@7.0.1: {}
esbuild@0.21.5:
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
estree-walker@2.0.2: {}
fsevents@2.3.3:
optional: true
hls.js@1.6.15: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
nanoid@3.3.11: {}
picocolors@1.1.1: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.57.1:
dependencies:
'@types/estree': 1.0.8
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.3
source-map-js@1.2.1: {}
vite@5.4.21:
dependencies:
esbuild: 0.21.5
postcss: 8.5.6
rollup: 4.57.1
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
'@vue/compiler-sfc': 3.5.27
'@vue/runtime-dom': 3.5.27
'@vue/server-renderer': 3.5.27(vue@3.5.27)
'@vue/shared': 3.5.27

View File

@ -1,171 +1,210 @@
<template>
<div class="iptv-app">
<!-- 三栏主布局 -->
<div class="main-container">
<!-- 左侧频道分组 -->
<aside class="sidebar-left">
<div class="sidebar-header">
<span class="icon">📡</span>
<span>频道分组</span>
</div>
<div class="group-list">
<div
v-for="group in groupList"
:key="group.id"
class="group-item"
:class="{ active: selectedGroup === group.id }"
@click="selectGroup(group.id)"
>
<span class="group-icon">{{ group.icon }}</span>
<span class="group-name">{{ group.name }}</span>
<span class="group-count">{{ group.count }}</span>
<span class="arrow"></span>
</div>
</div>
</aside>
<!-- 中间频道列表 -->
<section class="channel-panel">
<div class="panel-header">
<span class="icon"></span>
<span>{{ currentGroupName }}</span>
</div>
<div class="channel-list">
<div
v-for="channel in filteredChannels"
:key="channel.id"
class="channel-item"
:class="{ active: currentChannel?.id === channel.id }"
@click="playChannel(channel)"
>
<div class="channel-icon">CC</div>
<div class="channel-info">
<div class="channel-name">{{ channel.name }}</div>
<div class="channel-meta">{{ channel.sources?.length || 1 }}线路</div>
</div>
<span
class="star-icon"
:class="{ favorited: isFavorite(channel.id) }"
@click.stop="toggleFavorite(channel.id)"
></span>
</div>
</div>
</section>
<!-- 右侧节目表/播放器 -->
<section class="epg-panel">
<div class="panel-header">
<span class="icon">📋</span>
<span>节目表</span>
</div>
<!-- 节目列表 -->
<div v-if="currentChannel && epgList.length > 0" class="epg-list">
<div
v-for="(item, index) in epgList"
:key="index"
class="epg-item"
:class="{ current: isCurrentProgram(item) }"
>
<div class="epg-time">{{ item.time }}</div>
<div class="epg-title">
{{ item.title }}
<span v-if="isCurrentProgram(item)" class="live-badge">直播</span>
</div>
</div>
</div>
<!-- 无节目信息 -->
<div v-else-if="currentChannel" class="epg-empty">
<p>暂无节目信息</p>
</div>
<!-- 未选择频道 -->
<div v-else class="player-placeholder">
<div class="placeholder-icon">📹</div>
<p>请选择频道</p>
<p class="hint">按菜单键打开频道列表</p>
</div>
</section>
<!-- 调试信息面板 -->
<div v-if="showDebug" class="debug-panel">
<div class="debug-header">
<span>调试信息</span>
<button @click="showDebug = false">×</button>
</div>
<div class="debug-content">
<p><strong>URL:</strong> {{ debugInfo.url }}</p>
<p><strong>协议:</strong> {{ debugInfo.protocol }}</p>
<p><strong>路径:</strong> {{ debugInfo.loadPath }}</p>
<p><strong>状态:</strong> {{ debugInfo.status }}</p>
<p><strong>错误:</strong> {{ debugInfo.error || '无' }}</p>
<hr>
<button @click="retryLoad">重新加载</button>
<button @click="copyDebug">复制日志</button>
</div>
</div>
<!-- 调试按钮 -->
<button v-if="!showDebug" class="debug-toggle" @click="showDebug = true">🐛</button>
<!-- 加载中 -->
<div v-if="loading" class="loading-screen">
<div class="spinner"></div>
<p>{{ loadingText }}</p>
<p class="debug-hint">长按屏幕显示调试信息</p>
</div>
<!-- 错误提示 -->
<div v-else-if="error" class="error-screen">
<div class="error-icon"></div>
<h3>加载失败</h3>
<p>{{ error }}</p>
<button @click="loadChannels">重试</button>
</div>
<!-- 主界面 -->
<template v-else>
<!-- 三栏主布局 -->
<div class="main-container">
<!-- 左侧频道分组 -->
<aside class="sidebar-left">
<div class="sidebar-header">
<span class="icon">📡</span>
<span>频道分组</span>
</div>
<div class="group-list">
<div
v-for="group in groupList"
:key="group.id"
class="group-item"
:class="{ active: selectedGroup === group.id }"
@click="selectGroup(group.id)"
>
<span class="group-icon">{{ group.icon }}</span>
<span class="group-name">{{ group.name }}</span>
<span class="group-count">{{ group.count }}</span>
<span class="arrow"></span>
</div>
</div>
</aside>
<!-- 底部播放控制栏 -->
<footer v-if="currentChannel" class="player-bar">
<div class="bar-left">
<div class="channel-logo">CC</div>
<div class="program-info">
<div class="channel-line">
<span class="channel-name">{{ currentChannel.name }}</span>
<span class="line-tag">线路 {{ currentLine }}/{{ currentChannel.sources?.length || 1 }}</span>
<!-- 中间频道列表 -->
<section class="channel-panel">
<div class="panel-header">
<span class="icon"></span>
<span>{{ currentGroupName }}</span>
</div>
<div class="program-title">
<span class="live-dot"></span>
{{ currentProgramTitle }}
<div class="channel-list">
<div
v-for="channel in filteredChannels"
:key="channel.id"
class="channel-item"
:class="{ active: currentChannel?.id === channel.id }"
@click="playChannel(channel)"
>
<div class="channel-icon">CC</div>
<div class="channel-info">
<div class="channel-name">{{ channel.name }}</div>
<div class="channel-meta">{{ channel.sources?.length || 1 }}线路</div>
</div>
<span
class="star-icon"
:class="{ favorited: isFavorite(channel.id) }"
@click.stop="toggleFavorite(channel.id)"
></span>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: programProgress + '%' }"></div>
<span class="time-label">{{ currentTime }} / {{ totalTime }}</span>
</section>
<!-- 右侧节目表/播放器 -->
<section class="epg-panel">
<div class="panel-header">
<span class="icon">📋</span>
<span>节目表</span>
</div>
<!-- 节目列表 -->
<div v-if="currentChannel && epgList.length > 0" class="epg-list">
<div
v-for="(item, index) in epgList"
:key="index"
class="epg-item"
:class="{ current: isCurrentProgram(item) }"
>
<div class="epg-time">{{ item.time }}</div>
<div class="epg-title">
{{ item.title }}
<span v-if="isCurrentProgram(item)" class="live-badge">直播</span>
</div>
</div>
</div>
<!-- 无节目信息 -->
<div v-else-if="currentChannel" class="epg-empty">
<p>暂无节目信息</p>
</div>
<!-- 未选择频道 -->
<div v-else class="player-placeholder">
<div class="placeholder-icon">📹</div>
<p>请选择频道</p>
<p class="hint">按菜单键打开频道列表</p>
</div>
</section>
</div>
<!-- 底部播放控制栏 -->
<footer v-if="currentChannel" class="player-bar">
<div class="bar-left">
<div class="channel-logo">CC</div>
<div class="program-info">
<div class="channel-line">
<span class="channel-name">{{ currentChannel.name }}</span>
<span class="line-tag">线路 {{ currentLine }}/{{ currentChannel.sources?.length || 1 }}</span>
</div>
<div class="program-title">
<span class="live-dot"></span>
{{ currentProgramTitle }}
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: programProgress + '%' }"></div>
<span class="time-label">{{ currentTime }} / {{ totalTime }}</span>
</div>
<div class="next-program">下一节目: {{ nextProgramTitle }}</div>
</div>
<div class="next-program">下一节目: {{ nextProgramTitle }}</div>
</div>
</div>
<div class="bar-right">
<button
class="bar-btn"
:class="{ active: isFavorite(currentChannel.id) }"
@click="toggleFavorite(currentChannel.id)"
>
<span class="icon"></span>
<span>{{ isFavorite(currentChannel.id) ? '已收藏' : '收藏' }}</span>
</button>
<button class="bar-btn" @click="showSourceSelector = true">
<span class="icon"></span>
<span>切换线路</span>
</button>
<button class="bar-btn" @click="showConfig = true">
<span class="icon"></span>
<span>设置</span>
</button>
</div>
</footer>
<div class="bar-right">
<button
class="bar-btn"
:class="{ active: isFavorite(currentChannel.id) }"
@click="toggleFavorite(currentChannel.id)"
>
<span class="icon"></span>
<span>{{ isFavorite(currentChannel.id) ? '已收藏' : '收藏' }}</span>
</button>
<button class="bar-btn" @click="showSourceSelector = true">
<span class="icon"></span>
<span>切换线路</span>
</button>
<button class="bar-btn" @click="showConfig = true">
<span class="icon"></span>
<span>设置</span>
</button>
</div>
</footer>
<!-- 播放器区域全屏或嵌入 -->
<div v-if="currentChannel" class="video-container">
<VideoPlayer
ref="playerRef"
:url="currentUrl"
:title="currentChannel.name"
<!-- 播放器区域全屏或嵌入 -->
<div v-if="currentChannel" class="video-container">
<VideoPlayer
ref="playerRef"
:url="currentUrl"
:title="currentChannel.name"
/>
</div>
<!-- 设置弹窗 -->
<ConfigPanel
:show="showConfig"
@close="showConfig = false"
@reload="loadChannels"
/>
</div>
<!-- 设置弹窗 -->
<ConfigPanel
:show="showConfig"
@close="showConfig = false"
@reload="loadChannels"
/>
<!-- 线路选择弹窗 -->
<div v-if="showSourceSelector" class="modal-overlay" @click="showSourceSelector = false">
<div class="modal-content source-modal" @click.stop>
<h3>切换线路</h3>
<div class="source-list">
<div
v-for="(src, index) in currentChannel?.sources"
:key="index"
class="source-item"
:class="{ active: currentUrl === src.url, online: src.status === 'online' }"
@click="switchSource(src)"
>
<span class="source-name">线路 {{ index + 1 }}</span>
<span v-if="src.status === 'online'" class="source-status online"> {{ src.delay }}ms</span>
<span v-else-if="src.status === 'offline'" class="source-status offline"> 离线</span>
<span v-else class="source-status"> 未检测</span>
<!-- 线路选择弹窗 -->
<div v-if="showSourceSelector" class="modal-overlay" @click="showSourceSelector = false">
<div class="modal-content source-modal" @click.stop>
<h3>切换线路</h3>
<div class="source-list">
<div
v-for="(src, index) in currentChannel?.sources"
:key="index"
class="source-item"
:class="{ active: currentUrl === src.url, online: src.status === 'online' }"
@click="switchSource(src)"
>
<span class="source-name">线路 {{ index + 1 }}</span>
<span v-if="src.status === 'online'" class="source-status online"> {{ src.delay }}ms</span>
<span v-else-if="src.status === 'offline'" class="source-status offline"> 离线</span>
<span v-else class="source-status"> 未检测</span>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
@ -185,6 +224,17 @@ const { toggleFavorite, isFavorite, addToHistory, settings } = store
const { isMobile } = useMobile()
// ============ ============
const loading = ref(true)
const loadingText = ref('加载中...')
const error = ref(null)
const showDebug = ref(false)
const debugInfo = ref({
url: '',
protocol: '',
loadPath: '',
status: '',
error: ''
})
const channels = ref([])
const currentChannel = ref(null)
const currentUrl = ref('')
@ -347,9 +397,72 @@ function generateMockEPG(channelId) {
// ============ ============
async function loadChannels() {
loading.value = true
error.value = null
loadingText.value = '加载中...'
//
debugInfo.value.url = window.location.href
debugInfo.value.protocol = window.location.protocol
debugInfo.value.status = '开始加载...'
debugInfo.value.error = ''
try {
const response = await fetch('/api/result.txt')
const text = await response.text()
let text = null
// Android WebView
const isAndroidLocal = window.location.protocol === 'file:' ||
window.location.href.includes('android_asset')
// Android
const hasAndroidInterface = typeof window.AndroidAsset !== 'undefined'
if (isAndroidLocal && hasAndroidInterface) {
// Android 使 JS
debugInfo.value.loadPath = 'AndroidAsset.readChannelData()'
try {
const result = window.AndroidAsset.readChannelData()
if (result && !result.startsWith('ERROR:')) {
text = result
debugInfo.value.status = 'Android 接口加载成功'
console.log('[IPTV] 从 Android 接口加载数据成功')
} else {
throw new Error(result)
}
} catch (e) {
debugInfo.value.error = `接口加载失败: ${e.message}`
console.log('[IPTV] Android 接口加载失败:', e.message)
}
} else if (isAndroidLocal) {
// Android fetch
debugInfo.value.loadPath = 'api/result.txt (fetch)'
debugInfo.value.error = 'Android 接口未找到'
} else {
//
debugInfo.value.loadPath = '/api/result.txt (网络)'
try {
const response = await fetch('/api/result.txt')
debugInfo.value.status = `响应状态: ${response.status}`
if (response.ok) {
text = await response.text()
debugInfo.value.status = '网络加载成功'
console.log('[IPTV] 从网络加载数据成功')
} else {
throw new Error(`HTTP ${response.status}`)
}
} catch (e) {
debugInfo.value.error = `网络加载失败: ${e.message}`
console.log('[IPTV] 网络加载失败:', e.message)
}
}
// 使
if (!text) {
debugInfo.value.status = '使用模拟数据'
console.log('[IPTV] 使用模拟数据')
text = generateMockData()
}
const rawChannels = parseTXT(text)
channels.value = mergeChannels(rawChannels)
console.log(`[IPTV] 加载了 ${channels.value.length} 个频道`)
@ -363,11 +476,56 @@ async function loadChannels() {
}
} catch (e) {
console.error('加载频道失败:', e)
error.value = e.message || '加载失败'
} finally {
loading.value = false
}
}
// 线
function generateMockData() {
return `
央视频道,#genre#
CCTV1,http://example.com/cctv1
CCTV2,http://example.com/cctv2
CCTV3,http://example.com/cctv3
卫视频道,#genre#
湖南卫视,http://example.com/hunan
浙江卫视,http://example.com/zhejiang
东方卫视,http://example.com/dongfang
`.trim()
}
function retryLoad() {
loadChannels()
}
function copyDebug() {
const info = JSON.stringify(debugInfo.value, null, 2)
navigator.clipboard?.writeText(info)
alert('调试信息已复制到剪贴板')
}
//
let pressTimer = null
function startPress() {
pressTimer = setTimeout(() => {
showDebug.value = true
}, 1000)
}
function endPress() {
clearTimeout(pressTimer)
}
onMounted(() => {
loadChannels()
//
document.addEventListener('touchstart', startPress)
document.addEventListener('touchend', endPress)
document.addEventListener('mousedown', startPress)
document.addEventListener('mouseup', endPress)
})
</script>
@ -381,6 +539,152 @@ onMounted(() => {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 调试面板 */
.debug-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1a1a1a;
border: 2px solid #ff6b6b;
border-radius: 12px;
padding: 16px;
z-index: 9999;
min-width: 300px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0,0,0,0.8);
}
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #333;
}
.debug-header span {
font-weight: bold;
color: #ff6b6b;
}
.debug-header button {
background: #333;
border: none;
color: #fff;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
}
.debug-content {
font-size: 13px;
line-height: 1.6;
}
.debug-content p {
margin-bottom: 6px;
word-break: break-all;
}
.debug-content strong {
color: #888;
}
.debug-content hr {
border: none;
border-top: 1px solid #333;
margin: 12px 0;
}
.debug-content button {
margin-right: 8px;
margin-top: 8px;
padding: 8px 16px;
background: #333;
border: 1px solid #555;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.debug-content button:hover {
background: #444;
}
.debug-toggle {
position: fixed;
bottom: 20px;
right: 20px;
width: 44px;
height: 44px;
border-radius: 50%;
background: #333;
border: 1px solid #555;
font-size: 20px;
cursor: pointer;
z-index: 9998;
opacity: 0.7;
}
.debug-hint {
font-size: 12px;
color: #666;
margin-top: 16px;
}
/* 加载中 */
.loading-screen, .error-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0a0a0a;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #333;
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-screen p, .error-screen p {
color: #888;
margin-bottom: 16px;
}
.error-screen .error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-screen h3 {
color: #fff;
margin-bottom: 8px;
}
.error-screen button {
padding: 10px 24px;
background: #fff;
color: #000;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
/* ========== 三栏布局 ========== */
.main-container {
flex: 1;

View File

@ -5,6 +5,6 @@ export default defineConfig({
plugins: [vue()],
base: './',
build: {
outDir: '../desktop/dist-web'
outDir: 'dist-web'
}
})

7
web/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md

32
web/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# 多阶段构建
# 阶段1构建前端
FROM node:18-alpine AS builder
WORKDIR /app
# 复制前端代码
COPY ../ui ./ui
WORKDIR /app/ui
RUN npm install && npm run build
# 阶段2运行服务端
FROM node:18-alpine
WORKDIR /app
# 安装依赖
COPY package*.json ./
RUN npm install --production
# 复制服务端代码
COPY src ./src
# 复制构建好的前端
COPY --from=builder /app/ui/dist-web ./public
# 暴露端口
EXPOSE 3000
# 启动
CMD ["node", "src/index.js"]

84
web/README.md Normal file
View File

@ -0,0 +1,84 @@
# IPTV Web Docker 版
基于 Node.js + Express 的 IPTV Web 服务端,提供:
- 📺 频道列表 API
- 🔀 M3U8/HLS 流代理(解决跨域)
- 📦 Docker 一键部署
- 🚀 可选 Nginx 反向代理
## 快速开始
### 1. 构建并启动
```bash
# 基础版本
docker-compose up -d
# 带 Nginx 的版本
docker-compose --profile nginx up -d
```
### 2. 访问
```
http://localhost:3000 # 直接访问 Node 服务
http://localhost # 通过 Nginx如果启用
```
## API 接口
| 接口 | 说明 |
|------|------|
| `GET /health` | 健康检查 |
| `GET /api/channels` | 获取频道列表 |
| `GET /proxy/m3u8?url=xxx` | 代理 M3U8 播放列表 |
| `GET /proxy/ts?url=xxx` | 代理 TS 视频片段 |
| `GET /proxy/stream?url=xxx` | 通用流代理 |
## 目录结构
```
web/
├── src/
│ └── index.js # Express 服务端
├── nginx/
│ └── nginx.conf # Nginx 配置
├── public/ # 前端静态文件(构建时复制)
├── Dockerfile # Docker 构建
├── docker-compose.yml # 编排配置
└── README.md
```
## 更新频道数据
频道数据来自 `ui/dist-web/api/result.txt`,更新方式:
```bash
# 方法1重新构建
docker-compose up -d --build
# 方法2挂载数据卷已配置
# 修改 ui/dist-web/api/result.txt 后自动生效
```
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `PORT` | 3000 | 服务端口号 |
| `NODE_ENV` | production | 运行环境 |
## 单独运行(不依赖 Docker
```bash
cd web
npm install
npm start
```
## 注意事项
1. **跨域问题** - 所有流媒体通过 `/proxy/*` 接口代理,解决浏览器 CORS 限制
2. **性能优化** - 静态资源使用 Nginx 缓存API 请求直连 Node
3. **HTTPS** - 生产环境建议启用 HTTPS配置 `nginx/ssl/` 目录)

36
web/docker-compose.yml Normal file
View File

@ -0,0 +1,36 @@
version: '3.8'
services:
iptv-web:
build: .
container_name: iptv-web
ports:
- "3000:3000"
environment:
- PORT=3000
- NODE_ENV=production
volumes:
# 挂载频道数据(可选,用于更新)
- ../ui/dist-web/api:/app/public/api:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# 可选:使用 Nginx 作为反向代理
nginx:
image: nginx:alpine
container_name: iptv-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- iptv-web
restart: unless-stopped
profiles:
- nginx

87
web/nginx/nginx.conf Normal file
View File

@ -0,0 +1,87 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript text/xml;
# 上游服务
upstream iptv_backend {
server iptv-web:3000;
}
# HTTP 服务器
server {
listen 80;
server_name localhost;
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
proxy_pass http://iptv_backend;
expires 1d;
add_header Cache-Control "public, immutable";
}
# API 和代理
location / {
proxy_pass http://iptv_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 流媒体代理(长连接)
location /proxy/ {
proxy_pass http://iptv_backend;
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 长超时(流媒体需要)
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
}
# HTTPS 服务器(需要证书)
server {
listen 443 ssl http2;
server_name localhost;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
proxy_pass http://iptv_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

20
web/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "iptv-web-server",
"version": "1.0.0",
"description": "IPTV Web 服务端 - 代理 M3U 和流媒体",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"http-proxy-middleware": "^2.0.6",
"node-fetch": "^2.7.0",
"m3u8-parser": "^7.1.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

1004
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

155
web/src/index.js Normal file
View File

@ -0,0 +1,155 @@
const express = require('express');
const cors = require('cors');
const { createProxyMiddleware } = require('http-proxy-middleware');
const fetch = require('node-fetch');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
// 静态文件服务
app.use(express.static(path.join(__dirname, '../public')));
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
// 获取频道列表
app.get('/api/channels', async (req, res) => {
try {
const dataDir = path.join(__dirname, '../../ui/dist-web/api');
const filePath = path.join(dataDir, 'result.txt');
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(content);
} else {
res.status(404).json({ error: 'Channel data not found' });
}
} catch (error) {
console.error('Error reading channels:', error);
res.status(500).json({ error: error.message });
}
});
// 代理 M3U8 流(解决跨域)
app.get('/proxy/m3u8', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'Missing url parameter' });
}
try {
console.log(`[Proxy] M3U8: ${url}`);
const response = await fetch(url, {
headers: {
'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0',
'Referer': new URL(url).origin
},
timeout: 10000
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const content = await response.text();
// 转换相对路径为绝对路径
const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
const modifiedContent = content.replace(
/^(?!#)([^\s].*\.ts.*)$/gm,
(match) => {
if (match.startsWith('http')) return match;
return `/proxy/ts?url=${encodeURIComponent(baseUrl + match)}`;
}
);
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'no-cache');
res.send(modifiedContent);
} catch (error) {
console.error('[Proxy] Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// 代理 TS 流
app.get('/proxy/ts', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'Missing url parameter' });
}
try {
const response = await fetch(url, {
headers: {
'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
response.headers.forEach((value, name) => {
res.setHeader(name, value);
});
response.body.pipe(res);
} catch (error) {
console.error('[Proxy] TS Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// 通用代理(用于其他类型的流)
app.get('/proxy/stream', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'Missing url parameter' });
}
try {
console.log(`[Proxy] Stream: ${url}`);
const response = await fetch(url, {
headers: {
'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
response.headers.forEach((value, name) => {
res.setHeader(name, value);
});
response.body.pipe(res);
} catch (error) {
console.error('[Proxy] Stream Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 IPTV Web Server running on port ${PORT}`);
console.log(`📺 Access: http://localhost:${PORT}`);
});