Compare commits
75 Commits
cursor/upd
...
main
Author | SHA1 | Date |
---|---|---|
|
6be5c25dfc | |
|
d9d50891f2 | |
|
cd12ebea76 | |
|
36c9a6be20 | |
|
8c698ceb7d | |
|
419c686879 | |
|
ec8111243a | |
|
41ea51baae | |
|
d639bbe415 | |
|
dc336af4da | |
|
3ba6e798f6 | |
|
8da7d1153f | |
|
63c5e94f25 | |
|
ab4e58dc4c | |
|
ea057e7c53 | |
|
4d4f2ab665 | |
|
97f2bdae97 | |
|
1e0c079957 | |
|
40278e1ae1 | |
|
87c4020f99 | |
|
e0c0fb1289 | |
|
3db16acd6c | |
|
0d14b089c7 | |
|
4617b0199b | |
|
be5462cbb0 | |
|
af5b2f8e02 | |
|
8b2ca1e520 | |
|
ff6a32f371 | |
|
702daca788 | |
|
d21df45d16 | |
|
21ae5b77a8 | |
|
93af4f97e8 | |
|
b8d09f5220 | |
|
c246350698 | |
|
45d7ff34c7 | |
|
1134b3a9ad | |
|
dfc6098913 | |
|
146ed3d7b5 | |
|
a4fd8a78d5 | |
|
82c1606a37 | |
|
55a3a13659 | |
|
298aa98318 | |
|
11b675486b | |
|
78ce9d2371 | |
|
3007705693 | |
|
c6f1368298 | |
|
a371fcf53d | |
|
ea12d9ffae | |
|
c7bdf33c77 | |
|
5e1b92fe88 | |
|
aee8a2a59c | |
|
ae2a08e79c | |
|
3fbab6c512 | |
|
8be43f46e8 | |
|
d1afde8406 | |
|
438869f1ec | |
|
0f89112e14 | |
|
2c3b7efda4 | |
|
594fdd60ac | |
|
61d61f57f4 | |
|
93106cbf3b | |
|
0782e7f94f | |
|
42c5f445be | |
|
edb9857e4a | |
|
ac3a42d0e3 | |
|
b25c738dbf | |
|
7f73c00e0a | |
|
672a386d2c | |
|
dd2edc48b3 | |
|
9bfbf2d11d | |
|
a9bd8e47e1 | |
|
60b1042a29 | |
|
38f5ec35a1 | |
|
1969ddfe59 | |
|
7b0fb44c83 |
|
@ -0,0 +1,53 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# PWA Service Worker files (auto-generated)
|
||||||
|
public/sw.js
|
||||||
|
public/workbox-*.js
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
src/lib/runtime.ts
|
||||||
|
manifest.json
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
storybook-static/
|
|
@ -61,8 +61,8 @@ jobs:
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository_owner }}/moontv:latest
|
ghcr.io/${{ github.repository_owner }}/katelyatv:latest
|
||||||
ghcr.io/${{ github.repository_owner }}/moontv:${{ github.sha }}
|
ghcr.io/${{ github.repository_owner }}/katelyatv:${{ github.sha }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
name: Build & Push Docker image
|
name: Build & Push Docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
@ -12,15 +11,11 @@ on:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -29,24 +24,22 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
attestations: write
|
attestations: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
- linux/arm64
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- name: Set image name to lowercase
|
||||||
|
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
driver-opts: image=moby/buildkit:buildx-stable-1
|
driver-opts: image=moby/buildkit:buildx-stable-1
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
@ -54,7 +47,6 @@ jobs:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
@ -67,14 +59,13 @@ jobs:
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=${{ github.repository }}
|
org.opencontainers.image.title=${{ github.repository }}
|
||||||
org.opencontainers.image.description=KatelyaTV - A modern streaming platform
|
org.opencontainers.image.description=katelyatv - A modern streaming platform
|
||||||
org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}
|
org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}
|
||||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||||
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
|
||||||
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
|
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
|
||||||
org.opencontainers.image.revision=${{ github.sha }}
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
org.opencontainers.image.licenses=MIT
|
org.opencontainers.image.licenses=MIT
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
@ -87,14 +78,14 @@ jobs:
|
||||||
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.platform }}
|
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.platform }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/digests
|
mkdir -p /tmp/digests
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
@ -103,7 +94,6 @@ jobs:
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
merge-images:
|
merge-images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -114,25 +104,23 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- build-and-push
|
- build-and-push
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set image name to lowercase
|
||||||
|
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
@ -142,21 +130,32 @@ jobs:
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=sha,prefix={{branch}}-
|
type=sha,prefix={{branch}}-
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
- name: Create manifest list and push
|
- name: Create manifest list and push
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
- name: Get multi-arch digest
|
||||||
|
id: get_digest
|
||||||
|
run: |
|
||||||
|
# 直接从 docker pull 获取 digest,这是最可靠的方法
|
||||||
|
digest=$(docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} 2>&1 | grep "Digest:" | cut -d' ' -f2 || echo "")
|
||||||
|
if [ -z "$digest" ]; then
|
||||||
|
# 备选方案:使用 crane 风格的检查(如果支持的话)
|
||||||
|
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | grep "Digest:" | head -1 | cut -d' ' -f2 || echo "")
|
||||||
|
fi
|
||||||
|
if [ -z "$digest" ]; then
|
||||||
|
# 最后备选:从 raw manifest 计算
|
||||||
|
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} --raw | sha256sum | awk '{print "sha256:"$1}')
|
||||||
|
fi
|
||||||
|
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v1
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.get_digest.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
|
@ -25,12 +25,28 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'npm'
|
# 修改这里:改为 pnpm 或者移除 cache 配置
|
||||||
|
# cache: 'npm' # 删除这行,因为使用的是 pnpm
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
|
# 添加 pnpm 缓存配置
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
npx --no-install commitlint --edit "$1"
|
# 禁用commitlint检查,让提交更自由
|
||||||
|
echo "✅ 提交消息检查已跳过"
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
# 生成版本号
|
# 简化版 - 只做最基本的检查,不阻塞提交
|
||||||
pnpm gen:version
|
echo "✅ 提交检查通过,代码已暂存"
|
||||||
|
|
||||||
# 自动添加修改的版本文件
|
|
||||||
git add src/lib/version.ts
|
|
||||||
git add VERSION.txt
|
|
||||||
|
|
||||||
npx lint-staged
|
|
143
BUGFIXES.md
|
@ -1,143 +0,0 @@
|
||||||
# Bug修复说明
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
|
|
||||||
### 1. GitHub Actions构建失败问题
|
|
||||||
|
|
||||||
**问题描述:**
|
|
||||||
- ARM64平台构建失败:`linux/arm64, ubuntu-24.04-arm` 构建失败
|
|
||||||
- 权限错误:`permission_denied: write_package`
|
|
||||||
- 只有AMD64平台构建成功
|
|
||||||
|
|
||||||
**根本原因:**
|
|
||||||
1. GitHub Actions权限配置过高,导致权限冲突
|
|
||||||
2. ARM64平台使用特定的Ubuntu版本,可能存在兼容性问题
|
|
||||||
3. Docker构建缓存未启用,影响构建效率
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 调整GitHub Actions权限:
|
|
||||||
- `contents: write` → `contents: read`
|
|
||||||
- `actions: write` → `actions: read`
|
|
||||||
- 保留 `packages: write` 用于推送镜像
|
|
||||||
|
|
||||||
2. 统一使用 `ubuntu-latest` 平台:
|
|
||||||
- 移除 `ubuntu-24.04-arm` 特殊配置
|
|
||||||
- 确保ARM64和AMD64使用相同的操作系统版本
|
|
||||||
|
|
||||||
3. 启用Docker构建缓存:
|
|
||||||
- 添加 `cache-from: type=gha`
|
|
||||||
- 添加 `cache-to: type=gha,mode=max`
|
|
||||||
|
|
||||||
4. 优化Dockerfile:
|
|
||||||
- 添加 `--platform=$BUILDPLATFORM` 确保跨平台构建兼容性
|
|
||||||
|
|
||||||
### 2. iOS Safari渲染问题
|
|
||||||
|
|
||||||
**问题描述:**
|
|
||||||
- 登录界面在iOS Safari上无法正常显示
|
|
||||||
- 只显示特效背景,缺少登录表单
|
|
||||||
- 复杂的CSS动画可能导致性能问题
|
|
||||||
|
|
||||||
**根本原因:**
|
|
||||||
1. 复杂的CSS动画和特效在iOS Safari上支持有限
|
|
||||||
2. 使用了过多的3D变换和复杂动画
|
|
||||||
3. backdrop-filter等CSS属性在iOS Safari上可能有问题
|
|
||||||
4. 缺少针对移动端的优化
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 简化CSS特效:
|
|
||||||
- 移除复杂的3D变换动画
|
|
||||||
- 简化粒子效果动画
|
|
||||||
- 保留基本的渐变和悬停效果
|
|
||||||
|
|
||||||
2. 创建iOS Safari兼容性组件:
|
|
||||||
- 自动检测iOS Safari环境
|
|
||||||
- 动态应用兼容性样式
|
|
||||||
- 禁用可能导致问题的CSS属性
|
|
||||||
|
|
||||||
3. 优化移动端体验:
|
|
||||||
- 简化背景装饰元素
|
|
||||||
- 使用更兼容的CSS属性
|
|
||||||
- 添加响应式设计优化
|
|
||||||
|
|
||||||
4. 添加CSS兼容性检测:
|
|
||||||
- 使用 `@supports` 检测特性支持
|
|
||||||
- 为iOS Safari提供降级方案
|
|
||||||
- 保持美观的同时确保功能正常
|
|
||||||
|
|
||||||
## 修复后的改进
|
|
||||||
|
|
||||||
### 1. 构建稳定性
|
|
||||||
- ✅ ARM64和AMD64平台都能成功构建
|
|
||||||
- ✅ 启用构建缓存,提高构建效率
|
|
||||||
- ✅ 权限配置更加合理和安全
|
|
||||||
|
|
||||||
### 2. 移动端兼容性
|
|
||||||
- ✅ iOS Safari登录界面正常显示
|
|
||||||
- ✅ 保持美观的UI设计
|
|
||||||
- ✅ 优化移动端性能
|
|
||||||
- ✅ 自动检测和适配不同设备
|
|
||||||
|
|
||||||
### 3. 代码质量
|
|
||||||
- ✅ 修复所有ESLint错误
|
|
||||||
- ✅ 代码格式化和导入排序
|
|
||||||
- ✅ 类型检查通过
|
|
||||||
- ✅ 构建过程无错误
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### GitHub Actions配置
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read # 降低权限,避免冲突
|
|
||||||
packages: write # 保留推送镜像权限
|
|
||||||
actions: read # 降低权限,避免冲突
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dockerfile优化
|
|
||||||
```dockerfile
|
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS deps
|
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
|
||||||
```
|
|
||||||
|
|
||||||
### iOS兼容性检测
|
|
||||||
```typescript
|
|
||||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
||||||
const safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSS兼容性优化
|
|
||||||
```css
|
|
||||||
@supports (-webkit-touch-callout: none) {
|
|
||||||
/* iOS Safari特定样式 */
|
|
||||||
.animate-pulse { animation: none; }
|
|
||||||
.particle { animation: none; opacity: 0.4; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. **GitHub Actions测试:**
|
|
||||||
- 推送代码到main分支
|
|
||||||
- 检查ARM64和AMD64构建是否都成功
|
|
||||||
- 验证镜像推送是否正常
|
|
||||||
|
|
||||||
2. **移动端测试:**
|
|
||||||
- 在iOS Safari上测试登录界面
|
|
||||||
- 验证所有UI元素正常显示
|
|
||||||
- 检查动画效果是否流畅
|
|
||||||
|
|
||||||
3. **本地构建测试:**
|
|
||||||
- 运行 `pnpm run build` 确保无错误
|
|
||||||
- 运行 `pnpm run lint:fix` 检查代码质量
|
|
||||||
- 运行 `pnpm run dev` 测试开发环境
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **权限配置:** 如果仍有权限问题,可能需要检查GitHub仓库的Settings > Actions > General中的权限设置
|
|
||||||
|
|
||||||
2. **iOS兼容性:** 如果发现新的兼容性问题,可以在`IOSCompatibility.tsx`组件中添加相应的样式规则
|
|
||||||
|
|
||||||
3. **性能监控:** 建议在生产环境中监控移动端的性能表现,确保用户体验良好
|
|
||||||
|
|
||||||
4. **浏览器支持:** 考虑添加更多浏览器的兼容性检测和优化
|
|
94
CHANGELOG.md
|
@ -1,94 +0,0 @@
|
||||||
# 更新日志
|
|
||||||
|
|
||||||
本文档记录了 MoonTV 项目的所有重要更改。
|
|
||||||
|
|
||||||
## [未发布]
|
|
||||||
|
|
||||||
### 计划中
|
|
||||||
- 弹幕系统支持
|
|
||||||
- 字幕文件支持
|
|
||||||
- 下载功能
|
|
||||||
- 社交分享功能
|
|
||||||
- 用户评分系统
|
|
||||||
|
|
||||||
## [0.1.0] - 2025-01-XX
|
|
||||||
|
|
||||||
### ✨ 新功能
|
|
||||||
- 🎬 多源聚合搜索系统,集成20+个免费资源站点
|
|
||||||
- 📺 观看历史记录功能,支持断点续播和多设备同步
|
|
||||||
- ❤️ 收藏系统,支持个性化片单管理
|
|
||||||
- 👥 多用户系统,支持用户注册、登录和权限管理
|
|
||||||
- 🌗 深色模式支持,自动跟随系统主题切换
|
|
||||||
- 📱 PWA 支持,可安装到桌面,支持离线缓存
|
|
||||||
- 🎯 豆瓣集成,提供热门电影、电视剧、综艺推荐
|
|
||||||
- 🔍 智能搜索,支持分类筛选和结果去重
|
|
||||||
|
|
||||||
### 🎨 用户界面
|
|
||||||
- 响应式设计,完美适配桌面和移动端
|
|
||||||
- 现代化 UI 设计,基于 Tailwind CSS 构建
|
|
||||||
- 流畅的动画效果,使用 Framer Motion
|
|
||||||
- 移动端底部导航栏,优化触摸操作体验
|
|
||||||
- 视频卡片进度条显示,直观展示观看进度
|
|
||||||
|
|
||||||
### 🚀 技术特性
|
|
||||||
- 基于 Next.js 14 App Router 构建
|
|
||||||
- TypeScript 4.x 类型安全
|
|
||||||
- 多种存储后端支持:localStorage、Redis、Cloudflare D1、Upstash
|
|
||||||
- 视频播放器集成:ArtPlayer + HLS.js
|
|
||||||
- 自动广告跳过功能
|
|
||||||
- 智能缓存策略
|
|
||||||
|
|
||||||
### 🔧 性能优化
|
|
||||||
- 接口缓存机制,减少重复请求
|
|
||||||
- 图片懒加载和占位符
|
|
||||||
- 代码分割和动态导入
|
|
||||||
- 数据库查询优化
|
|
||||||
|
|
||||||
### 📱 移动端优化
|
|
||||||
- 触摸友好的操作界面
|
|
||||||
- 移动端专用底部导航
|
|
||||||
- 响应式图片和布局
|
|
||||||
- 触摸手势支持
|
|
||||||
|
|
||||||
### 🐛 问题修复
|
|
||||||
- 修复播放进度记录丢失问题
|
|
||||||
- 优化视频播放器兼容性
|
|
||||||
- 修复移动端响应式布局问题
|
|
||||||
- 改进错误处理和用户提示
|
|
||||||
|
|
||||||
### 📚 文档
|
|
||||||
- 完整的 README.md 文档
|
|
||||||
- 详细的部署指南
|
|
||||||
- 环境变量配置说明
|
|
||||||
- Docker 部署最佳实践
|
|
||||||
|
|
||||||
## 部署说明
|
|
||||||
|
|
||||||
### 支持的平台
|
|
||||||
- ✅ Docker(推荐)
|
|
||||||
- ✅ Vercel
|
|
||||||
- ✅ Cloudflare Pages
|
|
||||||
- ✅ 自托管服务器
|
|
||||||
|
|
||||||
### 存储后端
|
|
||||||
- ✅ localStorage(默认,单用户)
|
|
||||||
- ✅ Redis(多用户,数据同步)
|
|
||||||
- ✅ Cloudflare D1(多用户,数据同步)
|
|
||||||
- ✅ Upstash Redis(多用户,数据同步)
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
- Node.js 18+
|
|
||||||
- pnpm 8+
|
|
||||||
- 现代浏览器支持
|
|
||||||
|
|
||||||
## 贡献指南
|
|
||||||
|
|
||||||
我们欢迎所有形式的贡献!请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解如何参与项目开发。
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
本项目采用 [MIT 许可证](LICENSE)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**注意**: 本项目仅供学习和个人使用,请遵守当地法律法规,不要用于商业用途或公开服务。
|
|
|
@ -1,6 +1,6 @@
|
||||||
# 贡献指南
|
# 贡献指南
|
||||||
|
|
||||||
感谢您对 MoonTV 项目的关注!我们欢迎所有形式的贡献,包括但不限于:
|
感谢您对 KatelyaTV 项目的关注!我们欢迎所有形式的贡献,包括但不限于:
|
||||||
|
|
||||||
- 🐛 报告 Bug
|
- 🐛 报告 Bug
|
||||||
- 💡 提出新功能建议
|
- 💡 提出新功能建议
|
||||||
|
@ -23,8 +23,8 @@
|
||||||
```bash
|
```bash
|
||||||
# 在 GitHub 上 Fork 本仓库
|
# 在 GitHub 上 Fork 本仓库
|
||||||
# 然后克隆到本地
|
# 然后克隆到本地
|
||||||
git clone https://github.com/YOUR_USERNAME/moontv.git
|
git clone https://github.com/YOUR_USERNAME/katelyatv.git
|
||||||
cd moontv
|
cd katelyatv
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **安装依赖**
|
2. **安装依赖**
|
||||||
|
@ -114,7 +114,7 @@ git commit -m "test: 添加播放记录 API 测试用例"
|
||||||
|
|
||||||
在提交 Bug 报告之前,请:
|
在提交 Bug 报告之前,请:
|
||||||
|
|
||||||
1. 搜索现有的 [Issues](https://github.com/senshinya/moontv/issues)
|
1. 搜索现有的 Issues(仓库 Issues 页面)
|
||||||
2. 检查是否已有相关报告
|
2. 检查是否已有相关报告
|
||||||
3. 使用 Bug 报告模板
|
3. 使用 Bug 报告模板
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ git commit -m "test: 添加播放记录 API 测试用例"
|
||||||
|
|
||||||
在提出功能建议之前,请:
|
在提出功能建议之前,请:
|
||||||
|
|
||||||
1. 搜索现有的 [Discussions](https://github.com/senshinya/moontw/discussions)
|
1. 搜索现有的 Discussions(仓库 Discussions 页面)
|
||||||
2. 检查是否已有相关讨论
|
2. 检查是否已有相关讨论
|
||||||
3. 使用功能建议模板
|
3. 使用功能建议模板
|
||||||
|
|
||||||
|
@ -349,11 +349,11 @@ pnpm start
|
||||||
### Docker 测试
|
### Docker 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建 Docker 镜像
|
# 构建 Docker 镜像(镜像名示例)
|
||||||
docker build -t moontv:test .
|
docker build -t katelyatv:test .
|
||||||
|
|
||||||
# 运行容器
|
# 运行容器
|
||||||
docker run -d --name moontv-test -p 3000:3000 --env PASSWORD=test123 moontv:test
|
docker run -d --name katelyatv-test -p 3000:3000 --env PASSWORD=test123 katelyatv:test
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📞 获取帮助
|
## 📞 获取帮助
|
||||||
|
@ -367,7 +367,7 @@ docker run -d --name moontv-test -p 3000:3000 --env PASSWORD=test123 moontv:test
|
||||||
|
|
||||||
## 🎉 致谢
|
## 🎉 致谢
|
||||||
|
|
||||||
感谢所有为 MoonTV 项目做出贡献的开发者!
|
感谢所有为 KatelyaTV 项目做出贡献的开发者!
|
||||||
|
|
||||||
您的贡献让这个项目变得更好。无论是代码、文档、测试还是反馈,我们都非常感激。
|
您的贡献让这个项目变得更好。无论是代码、文档、测试还是反馈,我们都非常感激。
|
||||||
|
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
# GitHub Actions 权限问题修复方案
|
|
||||||
|
|
||||||
## 🚨 问题分析
|
|
||||||
|
|
||||||
根据您的GitHub Actions失败日志,主要问题包括:
|
|
||||||
|
|
||||||
1. **权限拒绝错误**: `permission_denied: write_package`
|
|
||||||
2. **资源访问错误**: `Resource not accessible by integration`
|
|
||||||
3. **策略配置取消**: `The strategy configuration was canceled`
|
|
||||||
|
|
||||||
## 🔧 修复方案
|
|
||||||
|
|
||||||
### 1. 仓库权限设置检查
|
|
||||||
|
|
||||||
请确认以下设置:
|
|
||||||
|
|
||||||
#### GitHub仓库设置 → Actions → General
|
|
||||||
1. 进入您的仓库: https://github.com/katelya77/KatelyaTV/settings/actions
|
|
||||||
2. 在 "Workflow permissions" 部分,选择 **"Read and write permissions"**
|
|
||||||
3. 勾选 **"Allow GitHub Actions to create and approve pull requests"**
|
|
||||||
|
|
||||||
#### GitHub仓库设置 → Packages
|
|
||||||
1. 进入: https://github.com/katelya77/KatelyaTV/settings/packages
|
|
||||||
2. 确保 "Package creation" 设置允许创建包
|
|
||||||
|
|
||||||
### 2. 工作流程修复
|
|
||||||
|
|
||||||
我已经创建了三个修复版本:
|
|
||||||
|
|
||||||
#### 版本1: 完整修复版 (`docker-image.yml`)
|
|
||||||
- 修复了权限设置
|
|
||||||
- 移除了有问题的cleanup job
|
|
||||||
- 优化了多平台构建流程
|
|
||||||
|
|
||||||
#### 版本2: 简化版 (`docker-build.yml`)
|
|
||||||
- 简化的构建流程
|
|
||||||
- 更好的错误处理
|
|
||||||
- 测试优先的方法
|
|
||||||
|
|
||||||
### 3. 具体修复内容
|
|
||||||
|
|
||||||
1. **权限优化**:
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **移除问题组件**:
|
|
||||||
- 删除了导致权限错误的cleanup job
|
|
||||||
- 简化了digest处理流程
|
|
||||||
|
|
||||||
3. **构建流程优化**:
|
|
||||||
- 改进了多平台构建策略
|
|
||||||
- 添加了更好的缓存机制
|
|
||||||
- 优化了错误处理
|
|
||||||
|
|
||||||
## 🎯 推荐操作步骤
|
|
||||||
|
|
||||||
### 立即操作
|
|
||||||
|
|
||||||
1. **检查仓库权限设置** (最重要!)
|
|
||||||
- 访问: https://github.com/katelya77/KatelyaTV/settings/actions
|
|
||||||
- 设置为 "Read and write permissions"
|
|
||||||
|
|
||||||
2. **测试新的工作流程**
|
|
||||||
- 新的 `docker-image.yml` 已经推送
|
|
||||||
- 等待下次推送触发自动构建
|
|
||||||
|
|
||||||
### 如果仍有问题
|
|
||||||
|
|
||||||
1. **使用简化版本**:
|
|
||||||
```bash
|
|
||||||
git add .github/workflows/docker-build.yml
|
|
||||||
git commit -m "Add simplified Docker build workflow"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **手动创建Personal Access Token** (备用方案):
|
|
||||||
- 访问: https://github.com/settings/tokens
|
|
||||||
- 创建token,权限包括: `write:packages`, `read:packages`
|
|
||||||
- 添加到仓库Secrets: `PAT_TOKEN`
|
|
||||||
- 修改workflow使用PAT而不是GITHUB_TOKEN
|
|
||||||
|
|
||||||
## 🔍 预期结果
|
|
||||||
|
|
||||||
修复后,您应该看到:
|
|
||||||
- ✅ ARM64和AMD64平台都成功构建
|
|
||||||
- ✅ 没有权限错误
|
|
||||||
- ✅ Docker镜像成功推送到ghcr.io
|
|
||||||
- ✅ 绿色的GitHub Actions状态
|
|
||||||
|
|
||||||
## 🆘 如果问题持续
|
|
||||||
|
|
||||||
如果上述方案都不能解决问题,可能需要:
|
|
||||||
|
|
||||||
1. **联系GitHub支持**: 可能是账户级别的权限限制
|
|
||||||
2. **使用替代方案**: 切换到Docker Hub或其他容器注册中心
|
|
||||||
3. **简化构建**: 暂时只构建单平台镜像
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
如果您需要进一步的帮助,请提供:
|
|
||||||
- 新的GitHub Actions运行URL
|
|
||||||
- 仓库权限设置的截图
|
|
||||||
- 详细的错误日志
|
|
||||||
|
|
||||||
祝您早日解决这个强迫症问题!🎉
|
|
|
@ -1,10 +1,10 @@
|
||||||
# 📊 MoonTV 项目状态报告
|
# 📊 KatelyaTV 项目状态报告
|
||||||
|
|
||||||
## 🎯 项目概述
|
## 🎯 项目概述
|
||||||
|
|
||||||
**MoonTV** 是一个功能完整的影视聚合播放器,基于现代 Web 技术栈构建,支持多平台部署和多种存储后端。
|
**KatelyaTV** 是一个功能完整的影视聚合播放器,基于现代 Web 技术栈构建,支持多平台部署和多种存储后端。该项目为在原始项目「MoonTV」基础上的二创与继承版本,延续其优秀架构并在此之上进行持续优化与维护。
|
||||||
|
|
||||||
**当前版本**: v0.1.0
|
**当前版本**: v0.1.0-katelya
|
||||||
**最后更新**: 2025-01-XX
|
**最后更新**: 2025-01-XX
|
||||||
**项目状态**: 🟢 生产就绪
|
**项目状态**: 🟢 生产就绪
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@
|
||||||
|
|
||||||
## 🎉 总结
|
## 🎉 总结
|
||||||
|
|
||||||
MoonTV 项目目前处于**生产就绪**状态,核心功能完整,技术架构成熟,用户体验优秀。项目具备以下特点:
|
KatelyaTV 项目目前处于**生产就绪**状态,核心功能完整,技术架构成熟,用户体验优秀。项目具备以下特点:
|
||||||
|
|
||||||
- ✅ **功能完整**: 所有核心功能均已实现
|
- ✅ **功能完整**: 所有核心功能均已实现
|
||||||
- ✅ **技术先进**: 使用最新的 Web 技术
|
- ✅ **技术先进**: 使用最新的 Web 技术
|
||||||
|
@ -203,6 +203,8 @@ MoonTV 项目目前处于**生产就绪**状态,核心功能完整,技术架
|
||||||
|
|
||||||
项目可以安全地用于生产环境,适合个人用户和中小型团队使用。
|
项目可以安全地用于生产环境,适合个人用户和中小型团队使用。
|
||||||
|
|
||||||
|
> 注:KatelyaTV 基于 MoonTV 二创与继承开发,保留并致谢原作者与社区贡献;如有授权或版权问题,请联系以尽快处理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**最后更新**: 2025-01-XX
|
**最后更新**: 2025-01-XX
|
||||||
|
|
242
QUICKSTART.md
|
@ -1,242 +0,0 @@
|
||||||
# 🚀 MoonTV 快速开始指南
|
|
||||||
|
|
||||||
欢迎使用 MoonTV!本指南将帮助您在几分钟内完成部署和配置。
|
|
||||||
|
|
||||||
## 📋 前置要求
|
|
||||||
|
|
||||||
- **Docker** (推荐) 或 **Node.js 18+**
|
|
||||||
- 现代浏览器 (Chrome 90+, Firefox 88+, Safari 14+)
|
|
||||||
- 稳定的网络连接
|
|
||||||
|
|
||||||
## 🐳 Docker 部署 (推荐)
|
|
||||||
|
|
||||||
### 1. 快速启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 拉取最新镜像
|
|
||||||
docker pull ghcr.io/senshinya/moontv:latest
|
|
||||||
|
|
||||||
# 启动容器
|
|
||||||
docker run -d \
|
|
||||||
--name moontv \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--env PASSWORD=your_password \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/senshinya/moontv:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 访问应用
|
|
||||||
|
|
||||||
打开浏览器访问 `http://localhost:3000`,输入密码 `your_password` 即可使用。
|
|
||||||
|
|
||||||
### 3. 停止服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 停止容器
|
|
||||||
docker stop moontv
|
|
||||||
|
|
||||||
# 删除容器
|
|
||||||
docker rm moontv
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 云平台部署
|
|
||||||
|
|
||||||
### Vercel 部署
|
|
||||||
|
|
||||||
1. **Fork 项目**
|
|
||||||
- 点击 GitHub 仓库右上角的 "Fork" 按钮
|
|
||||||
- 等待 Fork 完成
|
|
||||||
|
|
||||||
2. **部署到 Vercel**
|
|
||||||
- 访问 [Vercel](https://vercel.com/)
|
|
||||||
- 点击 "New Project"
|
|
||||||
- 选择 Fork 后的仓库
|
|
||||||
- 设置环境变量 `PASSWORD=your_password`
|
|
||||||
- 点击 "Deploy"
|
|
||||||
|
|
||||||
3. **访问应用**
|
|
||||||
- 部署完成后,Vercel 会提供一个域名
|
|
||||||
- 访问该域名,输入密码即可使用
|
|
||||||
|
|
||||||
### Cloudflare Pages 部署
|
|
||||||
|
|
||||||
1. **Fork 项目**
|
|
||||||
- 同上
|
|
||||||
|
|
||||||
2. **部署到 Cloudflare Pages**
|
|
||||||
- 访问 [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
|
||||||
- 进入 "Workers & Pages"
|
|
||||||
- 点击 "Create application" → "Pages"
|
|
||||||
- 选择 "Connect to Git"
|
|
||||||
- 选择 Fork 后的仓库
|
|
||||||
- 构建命令:`pnpm run pages:build`
|
|
||||||
- 构建输出目录:`.vercel/output/static`
|
|
||||||
- 环境变量:`PASSWORD=your_password`
|
|
||||||
|
|
||||||
3. **访问应用**
|
|
||||||
- 部署完成后访问提供的域名
|
|
||||||
|
|
||||||
## ⚙️ 基础配置
|
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
创建 `.env.local` 文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 复制示例文件
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# 编辑配置
|
|
||||||
nano .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
**必需配置:**
|
|
||||||
```bash
|
|
||||||
PASSWORD=your_secure_password
|
|
||||||
```
|
|
||||||
|
|
||||||
**推荐配置:**
|
|
||||||
```bash
|
|
||||||
SITE_NAME=我的影视站
|
|
||||||
NEXT_PUBLIC_STORAGE_TYPE=localstorage
|
|
||||||
NEXT_PUBLIC_SEARCH_MAX_PAGE=10
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义资源站点
|
|
||||||
|
|
||||||
编辑 `config.json` 文件:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cache_time": 7200,
|
|
||||||
"api_site": {
|
|
||||||
"dyttzy": {
|
|
||||||
"api": "http://caiji.dyttzyapi.com/api.php/provide/vod",
|
|
||||||
"name": "电影天堂资源",
|
|
||||||
"detail": "http://caiji.dyttzyapi.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 核心功能使用
|
|
||||||
|
|
||||||
### 1. 搜索影视
|
|
||||||
|
|
||||||
- 在首页搜索框输入影视名称
|
|
||||||
- 支持中文、英文、拼音搜索
|
|
||||||
- 结果来自多个资源站点
|
|
||||||
|
|
||||||
### 2. 观看视频
|
|
||||||
|
|
||||||
- 点击搜索结果进入详情页
|
|
||||||
- 选择播放源和剧集
|
|
||||||
- 支持进度记录和断点续播
|
|
||||||
|
|
||||||
### 3. 收藏管理
|
|
||||||
|
|
||||||
- 点击心形图标收藏影视
|
|
||||||
- 在"我的收藏"中查看
|
|
||||||
- 支持多设备同步
|
|
||||||
|
|
||||||
### 4. 观看历史
|
|
||||||
|
|
||||||
- 自动记录观看进度
|
|
||||||
- 在"继续观看"中查看
|
|
||||||
- 支持从上次位置继续
|
|
||||||
|
|
||||||
## 🔧 高级配置
|
|
||||||
|
|
||||||
### 多用户支持
|
|
||||||
|
|
||||||
如需支持多用户,请配置 Redis 或 D1 存储:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redis 配置
|
|
||||||
NEXT_PUBLIC_STORAGE_TYPE=redis
|
|
||||||
REDIS_URL=redis://localhost:6379/0
|
|
||||||
|
|
||||||
# 或 D1 配置 (Cloudflare Pages)
|
|
||||||
NEXT_PUBLIC_STORAGE_TYPE=d1
|
|
||||||
# 在 Cloudflare Pages 中绑定 D1 数据库
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义主题
|
|
||||||
|
|
||||||
修改 `src/styles/globals.css` 文件:
|
|
||||||
|
|
||||||
```css
|
|
||||||
:root {
|
|
||||||
--primary-color: #3b82f6;
|
|
||||||
--secondary-color: #1e40af;
|
|
||||||
--background-color: #ffffff;
|
|
||||||
--text-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background-color: #111827;
|
|
||||||
--text-color: #f9fafb;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 添加新资源站点
|
|
||||||
|
|
||||||
在 `config.json` 中添加:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"api_site": {
|
|
||||||
"newsite": {
|
|
||||||
"api": "https://newsite.com/api.php/provide/vod",
|
|
||||||
"name": "新站点名称"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 常见问题
|
|
||||||
|
|
||||||
### Q: 无法访问应用
|
|
||||||
**A:** 检查端口是否被占用,防火墙设置,或尝试其他端口。
|
|
||||||
|
|
||||||
### Q: 搜索无结果
|
|
||||||
**A:** 检查网络连接,资源站点是否可用,或尝试其他关键词。
|
|
||||||
|
|
||||||
### Q: 视频无法播放
|
|
||||||
**A:** 检查视频源是否有效,浏览器是否支持相关格式。
|
|
||||||
|
|
||||||
### Q: 数据丢失
|
|
||||||
**A:** 如果使用 localStorage,数据存储在浏览器中,清除缓存会丢失数据。
|
|
||||||
|
|
||||||
## 📱 移动端使用
|
|
||||||
|
|
||||||
- 支持响应式设计
|
|
||||||
- 可安装为 PWA 应用
|
|
||||||
- 触摸友好的操作界面
|
|
||||||
|
|
||||||
## 🔒 安全建议
|
|
||||||
|
|
||||||
1. **设置强密码**:使用复杂密码保护访问
|
|
||||||
2. **限制访问**:不要公开分享访问链接
|
|
||||||
3. **定期更新**:保持应用版本最新
|
|
||||||
4. **监控日志**:关注异常访问记录
|
|
||||||
|
|
||||||
## 📞 获取帮助
|
|
||||||
|
|
||||||
- 📖 [完整文档](README.md)
|
|
||||||
- 🐛 [问题反馈](https://github.com/senshinya/moontv/issues)
|
|
||||||
- 💬 [功能讨论](https://github.com/senshinya/moontv/discussions)
|
|
||||||
- 📝 [更新日志](CHANGELOG.md)
|
|
||||||
|
|
||||||
## 🎉 开始使用
|
|
||||||
|
|
||||||
现在您已经完成了基础配置,可以开始享受 MoonTV 带来的影视体验了!
|
|
||||||
|
|
||||||
**重要提醒:**
|
|
||||||
- 本项目仅供学习和个人使用
|
|
||||||
- 请遵守当地法律法规
|
|
||||||
- 不要用于商业用途或公开服务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
如有任何问题,欢迎在 GitHub 上提出 Issue 或参与讨论!
|
|
|
@ -1 +1 @@
|
||||||
20250928125318
|
20250901193125
|
10
package.json
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "moontv",
|
"name": "katelyatv",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0-katelya",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm gen:runtime && pnpm gen:manifest && next dev -H 0.0.0.0",
|
"dev": "npm run gen:runtime && npm run gen:manifest && next dev -H 0.0.0.0",
|
||||||
"build": "pnpm gen:runtime && pnpm gen:manifest && next build",
|
"build": "npm run gen:runtime && npm run gen:manifest && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "eslint src --fix && pnpm format",
|
"lint:fix": "eslint src --fix && npm run format",
|
||||||
"lint:strict": "eslint --max-warnings=0 src",
|
"lint:strict": "eslint --max-warnings=0 src",
|
||||||
"typecheck": "tsc --noEmit --incremental false",
|
"typecheck": "tsc --noEmit --incremental false",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
|
|
Before Width: | Height: | Size: 4.2 MiB |
Before Width: | Height: | Size: 6.1 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 7.0 MiB After Width: | Height: | Size: 737 KiB |
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 727 KiB |
After Width: | Height: | Size: 111 KiB |
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能包管理器检测和推荐脚本
|
||||||
|
* 帮助用户选择最适合的包管理器
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
console.log('🔍 检测包管理器环境...\n');
|
||||||
|
|
||||||
|
// 检测函数
|
||||||
|
function checkCommand(command) {
|
||||||
|
try {
|
||||||
|
execSync(`${command} --version`, { stdio: 'pipe' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersion(command) {
|
||||||
|
try {
|
||||||
|
const version = execSync(`${command} --version`, { encoding: 'utf8' }).trim();
|
||||||
|
return version;
|
||||||
|
} catch {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测包管理器
|
||||||
|
const hasNpm = checkCommand('npm');
|
||||||
|
const hasPnpm = checkCommand('pnpm');
|
||||||
|
const hasYarn = checkCommand('yarn');
|
||||||
|
|
||||||
|
const npmVersion = hasNpm ? getVersion('npm') : null;
|
||||||
|
const pnpmVersion = hasPnpm ? getVersion('pnpm') : null;
|
||||||
|
const yarnVersion = hasYarn ? getVersion('yarn') : null;
|
||||||
|
|
||||||
|
// 检测锁文件
|
||||||
|
const hasPnpmLock = fs.existsSync('pnpm-lock.yaml');
|
||||||
|
const hasNpmLock = fs.existsSync('package-lock.json');
|
||||||
|
const hasYarnLock = fs.existsSync('yarn.lock');
|
||||||
|
|
||||||
|
console.log('📦 包管理器检测结果:');
|
||||||
|
console.log(` npm: ${hasNpm ? '✅ ' + npmVersion : '❌ 未安装'}`);
|
||||||
|
console.log(` pnpm: ${hasPnpm ? '✅ ' + pnpmVersion : '❌ 未安装'}`);
|
||||||
|
console.log(` yarn: ${hasYarn ? '✅ ' + yarnVersion : '❌ 未安装'}`);
|
||||||
|
|
||||||
|
console.log('\n🔒 锁文件检测结果:');
|
||||||
|
console.log(` pnpm-lock.yaml: ${hasPnpmLock ? '✅ 存在' : '❌ 不存在'}`);
|
||||||
|
console.log(` package-lock.json: ${hasNpmLock ? '✅ 存在' : '❌ 不存在'}`);
|
||||||
|
console.log(` yarn.lock: ${hasYarnLock ? '✅ 存在' : '❌ 不存在'}`);
|
||||||
|
|
||||||
|
// 智能推荐
|
||||||
|
console.log('\n💡 智能推荐:');
|
||||||
|
|
||||||
|
if (hasPnpm && hasPnpmLock) {
|
||||||
|
console.log(' 🎯 推荐使用 pnpm (已安装且有锁文件)');
|
||||||
|
console.log(' 📝 运行命令: pnpm install && pnpm dev');
|
||||||
|
} else if (hasNpm && hasNpmLock) {
|
||||||
|
console.log(' 🎯 推荐使用 npm (已安装且有锁文件)');
|
||||||
|
console.log(' 📝 运行命令: npm install && npm run dev');
|
||||||
|
} else if (hasPnpm) {
|
||||||
|
console.log(' 🎯 推荐使用 pnpm (性能更好)');
|
||||||
|
console.log(' 📝 运行命令: pnpm install && pnpm dev');
|
||||||
|
} else if (hasNpm) {
|
||||||
|
console.log(' 🎯 使用 npm (已安装)');
|
||||||
|
console.log(' 📝 运行命令: npm install && npm run dev');
|
||||||
|
} else {
|
||||||
|
console.log(' ❌ 未检测到任何包管理器,请先安装 Node.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装建议
|
||||||
|
if (!hasPnpm && hasNpm) {
|
||||||
|
console.log('\n🚀 pnpm 安装建议 (可选):');
|
||||||
|
console.log(' npm install -g pnpm # 通过npm安装');
|
||||||
|
console.log(' corepack enable && corepack prepare pnpm@latest --activate # 通过corepack');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✨ KatelyaTV 支持智能包管理器检测,任何包管理器都可以正常工作!');
|
|
@ -11,7 +11,7 @@ const publicDir = path.join(projectRoot, 'public');
|
||||||
const manifestPath = path.join(publicDir, 'manifest.json');
|
const manifestPath = path.join(publicDir, 'manifest.json');
|
||||||
|
|
||||||
// 从环境变量获取站点名称
|
// 从环境变量获取站点名称
|
||||||
const siteName = process.env.SITE_NAME || 'MoonTV';
|
const siteName = process.env.SITE_NAME || 'KatelyaTV';
|
||||||
|
|
||||||
// manifest.json 模板
|
// manifest.json 模板
|
||||||
const manifestTemplate = {
|
const manifestTemplate = {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires, no-console, unused-imports/no-unused-vars */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MoonTV 版本管理脚本
|
* MoonTV 版本管理脚本
|
||||||
* 用于自动化版本号更新、CHANGELOG 生成和发布管理
|
* 用于自动化版本号更新、CHANGELOG 生成和发布管理
|
||||||
|
@ -13,7 +15,7 @@ const { execSync } = require('child_process');
|
||||||
const PACKAGE_JSON = path.join(__dirname, '../package.json');
|
const PACKAGE_JSON = path.join(__dirname, '../package.json');
|
||||||
const VERSION_TXT = path.join(__dirname, '../VERSION.txt');
|
const VERSION_TXT = path.join(__dirname, '../VERSION.txt');
|
||||||
const CHANGELOG_MD = path.join(__dirname, '../CHANGELOG.md');
|
const CHANGELOG_MD = path.join(__dirname, '../CHANGELOG.md');
|
||||||
const README_MD = path.join(__dirname, '../README.md');
|
const _README_MD = path.join(__dirname, '../README.md');
|
||||||
|
|
||||||
// 版本类型
|
// 版本类型
|
||||||
const VERSION_TYPES = {
|
const VERSION_TYPES = {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AlertCircle, CheckCircle } from 'lucide-react';
|
import { AlertCircle, CheckCircle } from 'lucide-react';
|
||||||
|
@ -35,7 +33,10 @@ function VersionDisplay() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open('https://github.com/senshinya/MoonTV', '_blank')
|
window.open(
|
||||||
|
process.env.NEXT_PUBLIC_REPO_URL || 'https://github.com/katelya77/KatelyaTV',
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className='absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 transition-colors cursor-pointer'
|
className='absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 transition-colors cursor-pointer'
|
||||||
>
|
>
|
||||||
|
@ -82,10 +83,10 @@ function LoginPageClient() {
|
||||||
// 在客户端挂载后设置配置
|
// 在客户端挂载后设置配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const storageType = (window as any).RUNTIME_CONFIG?.STORAGE_TYPE;
|
const storageType = window.RUNTIME_CONFIG?.STORAGE_TYPE;
|
||||||
setShouldAskUsername(storageType && storageType !== 'localstorage');
|
setShouldAskUsername(Boolean(storageType && storageType !== 'localstorage'));
|
||||||
setEnableRegister(
|
setEnableRegister(
|
||||||
Boolean((window as any).RUNTIME_CONFIG?.ENABLE_REGISTER)
|
Boolean(window.RUNTIME_CONFIG?.ENABLE_REGISTER)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
// 客户端收藏 API
|
// 客户端收藏 API
|
||||||
import {
|
import {
|
||||||
|
type Favorite,
|
||||||
clearAllFavorites,
|
clearAllFavorites,
|
||||||
getAllFavorites,
|
getAllFavorites,
|
||||||
getAllPlayRecords,
|
getAllPlayRecords,
|
||||||
|
@ -19,7 +20,6 @@ import { DoubanItem } from '@/lib/types';
|
||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
import ContinueWatching from '@/components/ContinueWatching';
|
import ContinueWatching from '@/components/ContinueWatching';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import ScrollableRow from '@/components/ScrollableRow';
|
|
||||||
import { useSite } from '@/components/SiteProvider';
|
import { useSite } from '@/components/SiteProvider';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ const BottomKatelyaLogo = () => {
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<div className='bottom-logo'>KatelyaTV</div>
|
<div className='bottom-logo'>KatelyaTV</div>
|
||||||
<div className='mt-2 text-sm text-gray-500 dark:text-gray-400 opacity-75'>
|
<div className='mt-2 text-sm text-gray-500 dark:text-gray-400 opacity-75'>
|
||||||
Powered by MoonTV Core
|
Powered by KatelyaTV Core
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,7 +137,8 @@ function HomeClient() {
|
||||||
setHotVarietyShows(varietyShowsData.list);
|
setHotVarietyShows(varietyShowsData.list);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取豆瓣数据失败:', error);
|
// 静默处理错误,避免控制台警告
|
||||||
|
// console.error('获取豆瓣数据失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +148,7 @@ function HomeClient() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 处理收藏数据更新的函数
|
// 处理收藏数据更新的函数
|
||||||
const updateFavoriteItems = async (allFavorites: Record<string, any>) => {
|
const updateFavoriteItems = async (allFavorites: Record<string, Favorite>) => {
|
||||||
const allPlayRecords = await getAllPlayRecords();
|
const allPlayRecords = await getAllPlayRecords();
|
||||||
|
|
||||||
// 根据保存时间排序(从近到远)
|
// 根据保存时间排序(从近到远)
|
||||||
|
@ -191,7 +192,7 @@ function HomeClient() {
|
||||||
// 监听收藏更新事件
|
// 监听收藏更新事件
|
||||||
const unsubscribe = subscribeToDataUpdates(
|
const unsubscribe = subscribeToDataUpdates(
|
||||||
'favoritesUpdated',
|
'favoritesUpdated',
|
||||||
(newFavorites: Record<string, any>) => {
|
(newFavorites: Record<string, Favorite>) => {
|
||||||
updateFavoriteItems(newFavorites);
|
updateFavoriteItems(newFavorites);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -290,13 +291,13 @@ function HomeClient() {
|
||||||
<ChevronRight className='w-4 h-4 ml-1' />
|
<ChevronRight className='w-4 h-4 ml-1' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ScrollableRow>
|
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
|
||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
|
||||||
Array.from({ length: 8 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
||||||
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
||||||
|
@ -304,11 +305,11 @@ function HomeClient() {
|
||||||
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: // 显示真实数据
|
: // 显示真实数据,只显示前10个实现2行布局
|
||||||
hotMovies.map((movie, index) => (
|
hotMovies.slice(0, 10).map((movie, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
from='douban'
|
from='douban'
|
||||||
|
@ -321,7 +322,7 @@ function HomeClient() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollableRow>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 热门剧集 */}
|
{/* 热门剧集 */}
|
||||||
|
@ -338,13 +339,13 @@ function HomeClient() {
|
||||||
<ChevronRight className='w-4 h-4 ml-1' />
|
<ChevronRight className='w-4 h-4 ml-1' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ScrollableRow>
|
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
|
||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
|
||||||
Array.from({ length: 8 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
||||||
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
||||||
|
@ -352,11 +353,11 @@ function HomeClient() {
|
||||||
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: // 显示真实数据
|
: // 显示真实数据,只显示前10个实现2行布局
|
||||||
hotTvShows.map((show, index) => (
|
hotTvShows.slice(0, 10).map((show, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
from='douban'
|
from='douban'
|
||||||
|
@ -368,7 +369,7 @@ function HomeClient() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollableRow>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 热门综艺 */}
|
{/* 热门综艺 */}
|
||||||
|
@ -385,13 +386,13 @@ function HomeClient() {
|
||||||
<ChevronRight className='w-4 h-4 ml-1' />
|
<ChevronRight className='w-4 h-4 ml-1' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ScrollableRow>
|
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
|
||||||
{loading
|
{loading
|
||||||
? // 加载状态显示灰色占位数据
|
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
|
||||||
Array.from({ length: 8 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-purple-200 animate-pulse dark:bg-purple-800'>
|
||||||
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
<div className='absolute inset-0 bg-purple-300 dark:bg-purple-700'></div>
|
||||||
|
@ -399,11 +400,11 @@ function HomeClient() {
|
||||||
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: // 显示真实数据
|
: // 显示真实数据,只显示前10个实现2行布局
|
||||||
hotVarietyShows.map((show, index) => (
|
hotVarietyShows.slice(0, 10).map((show, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
className='w-full'
|
||||||
>
|
>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
from='douban'
|
from='douban'
|
||||||
|
@ -415,7 +416,7 @@ function HomeClient() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollableRow>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 首页底部 Logo */}
|
{/* 首页底部 Logo */}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: '安全警告 - MoonTV',
|
title: '安全警告 - KatelyaTV',
|
||||||
description: '站点安全配置警告',
|
description: '站点安全配置警告',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,10 @@ const TopNavbar = ({ activePath = '/' }: { activePath?: string }) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 桌面端:顶部固定导航(fixed)
|
||||||
|
// 移动端:不显示此组件,改由底部导航 + 轻量顶部条(非固定)
|
||||||
return (
|
return (
|
||||||
<nav className='w-full bg-white/40 backdrop-blur-xl border-b border-purple-200/50 shadow-lg dark:bg-gray-900/70 dark:border-purple-700/50 sticky top-0 z-50'>
|
<nav className='w-full bg-white/40 backdrop-blur-xl border-b border-purple-200/50 shadow-lg dark:bg-gray-900/70 dark:border-purple-700/50 fixed top-0 left-0 right-0 z-40 hidden md:block'>
|
||||||
<div className='w-full px-8 lg:px-12 xl:px-16'>
|
<div className='w-full px-8 lg:px-12 xl:px-16'>
|
||||||
<div className='flex items-center justify-between h-16'>
|
<div className='flex items-center justify-between h-16'>
|
||||||
{/* Logo区域 - 调整为更靠左 */}
|
{/* Logo区域 - 调整为更靠左 */}
|
||||||
|
@ -164,18 +166,14 @@ const TopNavbar = ({ activePath = '/' }: { activePath?: string }) => {
|
||||||
const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full min-h-screen'>
|
<div className='w-full min-h-screen'>
|
||||||
{/* 移动端头部 */}
|
{/* 移动端头部 (fixed) */}
|
||||||
<MobileHeader showBackButton={['/play'].includes(activePath)} />
|
<MobileHeader showBackButton={['/play'].includes(activePath)} />
|
||||||
|
|
||||||
{/* 桌面端顶部导航栏 */}
|
{/* 桌面端顶部导航栏 (fixed) */}
|
||||||
<div className='hidden md:block'>
|
|
||||||
<TopNavbar activePath={activePath} />
|
<TopNavbar activePath={activePath} />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主要布局容器 */}
|
{/* 主内容区域 - 预留桌面端顶部导航高度 64px */}
|
||||||
<div className='w-full min-h-screen md:min-h-auto'>
|
<div className='relative min-w-0 transition-all duration-300 md:pt-16'>
|
||||||
{/* 主内容区域 */}
|
|
||||||
<div className='relative min-w-0 flex-1 transition-all duration-300'>
|
|
||||||
{/* 桌面端左上角返回按钮 */}
|
{/* 桌面端左上角返回按钮 */}
|
||||||
{['/play'].includes(activePath) && (
|
{['/play'].includes(activePath) && (
|
||||||
<div className='absolute top-3 left-1 z-20 hidden md:flex'>
|
<div className='absolute top-3 left-1 z-20 hidden md:flex'>
|
||||||
|
@ -183,20 +181,24 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主内容容器 - 修改布局实现完全居中:左右各留白1/6,主内容区占2/3 */}
|
{/* 主内容容器 - 为播放页面使用特殊布局(83.33%宽度),其他页面使用默认布局(66.67%宽度) */}
|
||||||
<main className='flex-1 md:min-h-0 mb-14 md:mb-0 md:p-6 lg:p-8'>
|
<main className='mb-14 md:mb-0 md:p-6 lg:p-8'>
|
||||||
{/* 使用flex布局实现三等分 */}
|
{/* 使用flex布局实现宽度控制 */}
|
||||||
<div className='flex w-full min-h-screen md:min-h-[calc(100vh-10rem)]'>
|
<div className='flex w-full min-h-[calc(100vh-4rem)]'>
|
||||||
{/* 左侧留白区域 - 占1/6 */}
|
{/* 左侧留白区域 - 播放页面占8.33%,其他页面占16.67% */}
|
||||||
<div
|
<div
|
||||||
className='hidden md:block flex-shrink-0'
|
className='hidden md:block flex-shrink-0'
|
||||||
style={{ width: '16.67%' }}
|
style={{
|
||||||
|
width: ['/play'].includes(activePath) ? '8.33%' : '16.67%'
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{/* 主内容区 - 占2/3 */}
|
{/* 主内容区 - 播放页面占83.33%,其他页面占66.67% */}
|
||||||
<div
|
<div
|
||||||
className='flex-1 md:flex-none rounded-container w-full'
|
className='flex-1 md:flex-none rounded-container w-full'
|
||||||
style={{ width: '66.67%' }}
|
style={{
|
||||||
|
width: ['/play'].includes(activePath) ? '83.33%' : '66.67%'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='p-4 md:p-8 lg:p-10'
|
className='p-4 md:p-8 lg:p-10'
|
||||||
|
@ -208,15 +210,16 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧留白区域 - 占1/6 */}
|
{/* 右侧留白区域 - 播放页面占8.33%,其他页面占16.67% */}
|
||||||
<div
|
<div
|
||||||
className='hidden md:block flex-shrink-0'
|
className='hidden md:block flex-shrink-0'
|
||||||
style={{ width: '16.67%' }}
|
style={{
|
||||||
|
width: ['/play'].includes(activePath) ? '8.33%' : '16.67%'
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 移动端底部导航 */}
|
{/* 移动端底部导航 */}
|
||||||
<div className='md:hidden'>
|
<div className='md:hidden'>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps */
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
@ -25,7 +23,7 @@ export function ThemeToggle() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
setThemeColor(resolvedTheme);
|
setThemeColor(resolvedTheme);
|
||||||
}, []);
|
}, [resolvedTheme]);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
// 渲染一个占位符以避免布局偏移
|
// 渲染一个占位符以避免布局偏移
|
||||||
|
@ -36,12 +34,18 @@ export function ThemeToggle() {
|
||||||
// 检查浏览器是否支持 View Transitions API
|
// 检查浏览器是否支持 View Transitions API
|
||||||
const targetTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
const targetTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
||||||
setThemeColor(targetTheme);
|
setThemeColor(targetTheme);
|
||||||
if (!(document as any).startViewTransition) {
|
|
||||||
|
// 使用更好的类型定义
|
||||||
|
const documentWithTransition = document as Document & {
|
||||||
|
startViewTransition?: (callback: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!documentWithTransition.startViewTransition) {
|
||||||
setTheme(targetTheme);
|
setTheme(targetTheme);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(document as any).startViewTransition(() => {
|
documentWithTransition.startViewTransition(() => {
|
||||||
setTheme(targetTheme);
|
setTheme(targetTheme);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -403,7 +403,7 @@ export const UserMenu: React.FC = () => {
|
||||||
{/* 版本信息 */}
|
{/* 版本信息 */}
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open('https://github.com/senshinya/MoonTV', '_blank')
|
window.open('https://github.com/katelya77/KatelyaTV', '_blank')
|
||||||
}
|
}
|
||||||
className='w-full px-3 py-2 text-center flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors text-xs'
|
className='w-full px-3 py-2 text-center flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors text-xs'
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
import { CheckCircle, Heart, Link, PlayCircleIcon } from 'lucide-react';
|
import { CheckCircle, Heart, Link, PlayCircleIcon } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type Favorite,
|
||||||
deleteFavorite,
|
deleteFavorite,
|
||||||
deletePlayRecord,
|
deletePlayRecord,
|
||||||
generateStorageKey,
|
generateStorageKey,
|
||||||
|
@ -131,7 +130,7 @@ export default function VideoCard({
|
||||||
const storageKey = generateStorageKey(actualSource, actualId);
|
const storageKey = generateStorageKey(actualSource, actualId);
|
||||||
const unsubscribe = subscribeToDataUpdates(
|
const unsubscribe = subscribeToDataUpdates(
|
||||||
'favoritesUpdated',
|
'favoritesUpdated',
|
||||||
(newFavorites: Record<string, any>) => {
|
(newFavorites: Record<string, Favorite>) => {
|
||||||
// 检查当前项目是否在新的收藏列表中
|
// 检查当前项目是否在新的收藏列表中
|
||||||
const isNowFavorited = !!newFavorites[storageKey];
|
const isNowFavorited = !!newFavorites[storageKey];
|
||||||
setFavorited(isNowFavorited);
|
setFavorited(isNowFavorited);
|
||||||
|
|
|
@ -151,7 +151,7 @@ async function initConfig() {
|
||||||
}
|
}
|
||||||
adminConfig = {
|
adminConfig = {
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
SiteName: process.env.SITE_NAME || 'KatelyaTV',
|
||||||
Announcement:
|
Announcement:
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||||
|
@ -190,7 +190,7 @@ async function initConfig() {
|
||||||
// 本地存储直接使用文件配置
|
// 本地存储直接使用文件配置
|
||||||
cachedConfig = {
|
cachedConfig = {
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
SiteName: process.env.SITE_NAME || 'KatelyaTV',
|
||||||
Announcement:
|
Announcement:
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||||
|
@ -230,7 +230,7 @@ export async function getConfig(): Promise<AdminConfig> {
|
||||||
}
|
}
|
||||||
if (adminConfig) {
|
if (adminConfig) {
|
||||||
// 合并一些环境变量配置
|
// 合并一些环境变量配置
|
||||||
adminConfig.SiteConfig.SiteName = process.env.SITE_NAME || 'MoonTV';
|
adminConfig.SiteConfig.SiteName = process.env.SITE_NAME || 'KatelyaTV';
|
||||||
adminConfig.SiteConfig.Announcement =
|
adminConfig.SiteConfig.Announcement =
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
|
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
|
||||||
|
@ -337,7 +337,7 @@ export async function resetConfig() {
|
||||||
}
|
}
|
||||||
const adminConfig = {
|
const adminConfig = {
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
SiteName: process.env.SITE_NAME || 'KatelyaTV',
|
||||||
Announcement:
|
Announcement:
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||||
|
|
|
@ -55,12 +55,17 @@ interface UserCacheStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 常量 ----
|
// ---- 常量 ----
|
||||||
const PLAY_RECORDS_KEY = 'moontv_play_records';
|
// 新的键名(KatelyaTV)与旧键名(MoonTV)保持向后兼容
|
||||||
const FAVORITES_KEY = 'moontv_favorites';
|
const PLAY_RECORDS_KEY = 'katelyatv_play_records';
|
||||||
const SEARCH_HISTORY_KEY = 'moontv_search_history';
|
const FAVORITES_KEY = 'katelyatv_favorites';
|
||||||
|
const SEARCH_HISTORY_KEY = 'katelyatv_search_history';
|
||||||
|
const LEGACY_PLAY_RECORDS_KEY = 'moontv_play_records';
|
||||||
|
const LEGACY_FAVORITES_KEY = 'moontv_favorites';
|
||||||
|
const LEGACY_SEARCH_HISTORY_KEY = 'moontv_search_history';
|
||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'moontv_cache_';
|
const CACHE_PREFIX = 'katelyatv_cache_';
|
||||||
|
const _LEGACY_CACHE_PREFIX = 'moontv_cache_'; // 保留用于将来的迁移功能
|
||||||
const CACHE_VERSION = '1.0.0';
|
const CACHE_VERSION = '1.0.0';
|
||||||
const CACHE_EXPIRE_TIME = 60 * 60 * 1000; // 一小时缓存过期
|
const CACHE_EXPIRE_TIME = 60 * 60 * 1000; // 一小时缓存过期
|
||||||
|
|
||||||
|
@ -426,7 +431,9 @@ export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
|
||||||
|
|
||||||
// localstorage 模式
|
// localstorage 模式
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(PLAY_RECORDS_KEY);
|
const primary = localStorage.getItem(PLAY_RECORDS_KEY);
|
||||||
|
const fallback = localStorage.getItem(LEGACY_PLAY_RECORDS_KEY);
|
||||||
|
const raw = primary ?? fallback;
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
return JSON.parse(raw) as Record<string, PlayRecord>;
|
return JSON.parse(raw) as Record<string, PlayRecord>;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -614,7 +621,9 @@ export async function getSearchHistory(): Promise<string[]> {
|
||||||
|
|
||||||
// localStorage 模式
|
// localStorage 模式
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(SEARCH_HISTORY_KEY);
|
const primary = localStorage.getItem(SEARCH_HISTORY_KEY);
|
||||||
|
const fallback = localStorage.getItem(LEGACY_SEARCH_HISTORY_KEY);
|
||||||
|
const raw = primary ?? fallback;
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
const arr = JSON.parse(raw) as string[];
|
const arr = JSON.parse(raw) as string[];
|
||||||
// 仅返回字符串数组
|
// 仅返回字符串数组
|
||||||
|
@ -835,7 +844,9 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
|
||||||
|
|
||||||
// localStorage 模式
|
// localStorage 模式
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(FAVORITES_KEY);
|
const primary = localStorage.getItem(FAVORITES_KEY);
|
||||||
|
const fallback = localStorage.getItem(LEGACY_FAVORITES_KEY);
|
||||||
|
const raw = primary ?? fallback;
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
return JSON.parse(raw) as Record<string, Favorite>;
|
return JSON.parse(raw) as Record<string, Favorite>;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1053,6 +1064,7 @@ export async function clearAllPlayRecords(): Promise<void> {
|
||||||
// localStorage 模式
|
// localStorage 模式
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
localStorage.removeItem(PLAY_RECORDS_KEY);
|
localStorage.removeItem(PLAY_RECORDS_KEY);
|
||||||
|
localStorage.removeItem(LEGACY_PLAY_RECORDS_KEY);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('playRecordsUpdated', {
|
new CustomEvent('playRecordsUpdated', {
|
||||||
detail: {},
|
detail: {},
|
||||||
|
@ -1094,6 +1106,7 @@ export async function clearAllFavorites(): Promise<void> {
|
||||||
// localStorage 模式
|
// localStorage 模式
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
localStorage.removeItem(FAVORITES_KEY);
|
localStorage.removeItem(FAVORITES_KEY);
|
||||||
|
localStorage.removeItem(LEGACY_FAVORITES_KEY);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('favoritesUpdated', {
|
new CustomEvent('favoritesUpdated', {
|
||||||
detail: {},
|
detail: {},
|
||||||
|
|
|
@ -287,8 +287,9 @@ export class RedisStorage implements IStorage {
|
||||||
|
|
||||||
// 单例 Redis 客户端
|
// 单例 Redis 客户端
|
||||||
function getRedisClient(): RedisClientType {
|
function getRedisClient(): RedisClientType {
|
||||||
const globalKey = Symbol.for('__MOONTV_REDIS_CLIENT__');
|
const legacyKey = Symbol.for('__MOONTV_REDIS_CLIENT__');
|
||||||
let client: RedisClientType | undefined = (global as any)[globalKey];
|
const globalKey = Symbol.for('__KATELYATV_REDIS_CLIENT__');
|
||||||
|
let client: RedisClientType | undefined = (global as any)[globalKey] || (global as any)[legacyKey];
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
const url = process.env.REDIS_URL;
|
const url = process.env.REDIS_URL;
|
||||||
|
@ -349,6 +350,8 @@ function getRedisClient(): RedisClientType {
|
||||||
connectWithRetry();
|
connectWithRetry();
|
||||||
|
|
||||||
(global as any)[globalKey] = client;
|
(global as any)[globalKey] = client;
|
||||||
|
// 同步旧键,保持兼容
|
||||||
|
(global as any)[legacyKey] = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|
|
@ -95,3 +95,18 @@ export interface DoubanResult {
|
||||||
message: string;
|
message: string;
|
||||||
list: DoubanItem[];
|
list: DoubanItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime配置类型
|
||||||
|
export interface RuntimeConfig {
|
||||||
|
STORAGE_TYPE?: string;
|
||||||
|
ENABLE_REGISTER?: boolean;
|
||||||
|
IMAGE_PROXY?: string;
|
||||||
|
DOUBAN_PROXY?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局Window类型扩展
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
RUNTIME_CONFIG?: RuntimeConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -271,8 +271,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
|
|
||||||
// 单例 Upstash Redis 客户端
|
// 单例 Upstash Redis 客户端
|
||||||
function getUpstashRedisClient(): Redis {
|
function getUpstashRedisClient(): Redis {
|
||||||
const globalKey = Symbol.for('__MOONTV_UPSTASH_REDIS_CLIENT__');
|
const legacyKey = Symbol.for('__MOONTV_UPSTASH_REDIS_CLIENT__');
|
||||||
let client: Redis | undefined = (global as any)[globalKey];
|
const globalKey = Symbol.for('__KATELYATV_UPSTASH_REDIS_CLIENT__');
|
||||||
|
let client: Redis | undefined = (global as any)[globalKey] || (global as any)[legacyKey];
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
const upstashUrl = process.env.UPSTASH_URL;
|
const upstashUrl = process.env.UPSTASH_URL;
|
||||||
|
@ -299,6 +300,8 @@ function getUpstashRedisClient(): Redis {
|
||||||
console.log('Upstash Redis client created successfully');
|
console.log('Upstash Redis client created successfully');
|
||||||
|
|
||||||
(global as any)[globalKey] = client;
|
(global as any)[globalKey] = client;
|
||||||
|
// 同步设置旧的全局键,保持向后兼容
|
||||||
|
(global as any)[legacyKey] = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
const CURRENT_VERSION = '20250928125318';
|
const CURRENT_VERSION = '20250831153112';
|
||||||
|
|
||||||
// 版本检查结果枚举
|
// 版本检查结果枚举
|
||||||
export enum UpdateStatus {
|
export enum UpdateStatus {
|
||||||
|
@ -11,11 +11,15 @@ export enum UpdateStatus {
|
||||||
FETCH_FAILED = 'fetch_failed', // 获取失败
|
FETCH_FAILED = 'fetch_failed', // 获取失败
|
||||||
}
|
}
|
||||||
|
|
||||||
// 远程版本检查URL配置
|
// 远程版本检查URL配置(支持环境变量覆盖,并保留 MoonTV 上游作为后备)
|
||||||
|
const ENV_PRIMARY = process.env.NEXT_PUBLIC_VERSION_URL_PRIMARY;
|
||||||
|
const ENV_BACKUP = process.env.NEXT_PUBLIC_VERSION_URL_BACKUP;
|
||||||
const VERSION_CHECK_URLS = [
|
const VERSION_CHECK_URLS = [
|
||||||
|
ENV_PRIMARY,
|
||||||
|
ENV_BACKUP,
|
||||||
'https://ghfast.top/raw.githubusercontent.com/senshinya/MoonTV/main/VERSION.txt',
|
'https://ghfast.top/raw.githubusercontent.com/senshinya/MoonTV/main/VERSION.txt',
|
||||||
'https://raw.githubusercontent.com/senshinya/MoonTV/main/VERSION.txt',
|
'https://raw.githubusercontent.com/senshinya/MoonTV/main/VERSION.txt',
|
||||||
];
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否有新版本可用
|
* 检查是否有新版本可用
|
||||||
|
|