Initial commit: ap_ds Code Repository System v1.0.0
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Repo data
|
||||
data/
|
||||
releases/
|
||||
branches/
|
||||
uploads/
|
||||
feedback.json
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# System files
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
desktop.ini
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 DVS (dvs-dvsxt)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 📚 ap_ds Code Repository System
|
||||
|
||||
> A self-hosted **code hosting & documentation platform** — code browsing, releases, version downloads, feedback, and admin management.
|
||||
|
||||
**ap_ds Code Repository System** is a Flask-based platform for hosting and showcasing code. It features code/file browsing with syntax highlighting, Markdown rendering, release management with version downloads (ZIP / TAR / WHL), search, user feedback, and a full admin dashboard.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 📂 **Code Browser** | Browse repository files / directories |
|
||||
| 👁️ **Code View** | Syntax highlighting (Pygments) |
|
||||
| 📄 **Markdown View** | Render Markdown documents |
|
||||
| 📦 **Release Downloads** | Download version archives (ZIP / TAR / WHL) |
|
||||
| 🔖 **Releases Page** | List versions & release notes |
|
||||
| 🔍 **Search** | Search across the repository |
|
||||
| 💬 **Feedback** | Users submit feedback; admin manages it |
|
||||
| 🛠️ **Admin Panel** | Login, dashboard, branches, version upload, feedback management |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Routes
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `/` | Homepage |
|
||||
| `/code` | Code browsing |
|
||||
| `/browse/<dir_path>` | Browse a directory |
|
||||
| `/view/<file_path>` | View a file (syntax highlight) |
|
||||
| `/download/<file_path>` | Download a file |
|
||||
| `/download/zip|tar|whl` | Download version archives |
|
||||
| `/releases` | Releases page |
|
||||
| `/search` | Search |
|
||||
| `/feedback` | Submit feedback |
|
||||
| `/admin/*` | Admin management |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8+
|
||||
- `flask`, `markdown`, `pygments`, `markupsafe`, `werkzeug`
|
||||
|
||||
### Install & Run
|
||||
|
||||
```bash
|
||||
pip install flask markdown pygments markupsafe werkzeug
|
||||
|
||||
python app.py
|
||||
```
|
||||
|
||||
Then open the site in your browser.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
AP_DS_GIT_CODE/
|
||||
├── app.py # Main Flask application
|
||||
├── static/
|
||||
│ ├── css.css # Stylesheet
|
||||
│ └── fonts/ # Icon fonts
|
||||
├── templates/ # 15 HTML templates
|
||||
│ ├── index.html # Homepage
|
||||
│ ├── code_browser.html # Code browser
|
||||
│ ├── admin_*.html # Admin pages
|
||||
│ └── ...
|
||||
└── README.md # This document
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
||||
@@ -0,0 +1,824 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ap_ds 代码仓库系统
|
||||
完整的代码托管与文档展示平台
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
import tarfile
|
||||
import hashlib
|
||||
import socket
|
||||
import mimetypes
|
||||
import threading
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
from flask import (
|
||||
Flask, render_template, send_file, request,
|
||||
redirect, url_for, session, jsonify, abort
|
||||
)
|
||||
from markupsafe import Markup
|
||||
from werkzeug.utils import secure_filename
|
||||
import markdown
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name, guess_lexer
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
||||
# ============================================
|
||||
# 应用初始化
|
||||
# ============================================
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "ap_ds_code_repo_secret_key_2026"
|
||||
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB
|
||||
|
||||
# 目录配置
|
||||
BASE_DIR = Path(__file__).parent
|
||||
CODE_DIR = BASE_DIR / "code"
|
||||
PYPI_DIR = BASE_DIR / "pypi"
|
||||
STATIC_DIR = BASE_DIR / "static"
|
||||
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||
DATA_DIR = BASE_DIR / "data"
|
||||
UPLOAD_DIR = BASE_DIR / "uploads"
|
||||
|
||||
# 创建必要目录
|
||||
for d in [CODE_DIR, PYPI_DIR, DATA_DIR, UPLOAD_DIR]:
|
||||
d.mkdir(exist_ok=True)
|
||||
|
||||
# 管理员密码
|
||||
ADMIN_PASSWORD_HASH = ""
|
||||
|
||||
def get_version_announcement(branch, version):
|
||||
"""获取版本的公告内容"""
|
||||
ann_path = CODE_DIR / branch / version / "ANNCMNT.md"
|
||||
if ann_path.exists():
|
||||
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
return render_markdown(content)
|
||||
return None
|
||||
|
||||
def get_version_readme(branch, version):
|
||||
"""获取版本的 README 内容"""
|
||||
readme_path = CODE_DIR / branch / version / "README.md"
|
||||
if readme_path.exists():
|
||||
with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
return render_markdown(content)
|
||||
return None
|
||||
|
||||
# ============================================
|
||||
# 数据存储辅助函数
|
||||
# ============================================
|
||||
def load_json(filepath, default=None):
|
||||
"""加载JSON文件"""
|
||||
if default is None:
|
||||
default = []
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return default
|
||||
|
||||
def save_json(filepath, data):
|
||||
"""保存JSON文件"""
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def load_feedbacks():
|
||||
"""加载反馈数据"""
|
||||
return load_json(DATA_DIR / "feedbacks.json", [])
|
||||
|
||||
def save_feedbacks(feedbacks):
|
||||
"""保存反馈数据"""
|
||||
save_json(DATA_DIR / "feedbacks.json", feedbacks)
|
||||
|
||||
def load_branches():
|
||||
"""加载分支列表"""
|
||||
branches = load_json(DATA_DIR / "branches.json", {})
|
||||
if not branches:
|
||||
# 扫描现有目录初始化
|
||||
for branch_dir in CODE_DIR.iterdir():
|
||||
if branch_dir.is_dir():
|
||||
versions = []
|
||||
for version_dir in branch_dir.iterdir():
|
||||
if version_dir.is_dir():
|
||||
versions.append(version_dir.name)
|
||||
branches[branch_dir.name] = sorted(versions, reverse=True)
|
||||
save_json(DATA_DIR / "branches.json", branches)
|
||||
return branches
|
||||
|
||||
def save_branches(branches):
|
||||
"""保存分支列表"""
|
||||
save_json(DATA_DIR / "branches.json", branches)
|
||||
|
||||
# ============================================
|
||||
# 邮件发送函数
|
||||
# ============================================
|
||||
def send_feedback_email(user_email, feedback_content):
|
||||
print(f"The original email sending function contains sensitive content. If you need it, please figure it out yourself.")
|
||||
|
||||
|
||||
# ============================================
|
||||
# 文件操作辅助函数
|
||||
# ============================================
|
||||
def get_branches_and_versions():
|
||||
"""获取所有分支和版本"""
|
||||
branches_data = {}
|
||||
for branch_dir in CODE_DIR.iterdir():
|
||||
if branch_dir.is_dir():
|
||||
versions = []
|
||||
for version_dir in sorted(branch_dir.iterdir(), reverse=True):
|
||||
if version_dir.is_dir():
|
||||
versions.append({
|
||||
"name": version_dir.name,
|
||||
"path": str(version_dir.relative_to(CODE_DIR)).replace('\\', '/'),
|
||||
"created": datetime.fromtimestamp(version_dir.stat().st_ctime).strftime("%Y-%m-%d")
|
||||
})
|
||||
if versions:
|
||||
branches_data[branch_dir.name] = versions
|
||||
return branches_data
|
||||
|
||||
def get_file_tree(branch, version):
|
||||
"""获取文件树结构"""
|
||||
version_path = CODE_DIR / branch / version
|
||||
if not version_path.exists():
|
||||
return []
|
||||
|
||||
tree = []
|
||||
for item in sorted(version_path.iterdir()):
|
||||
# 使用正斜杠统一路径分隔符
|
||||
path_str = str(item.relative_to(CODE_DIR)).replace('\\', '/')
|
||||
tree.append({
|
||||
"name": item.name,
|
||||
"type": "dir" if item.is_dir() else "file",
|
||||
"path": path_str,
|
||||
"size": item.stat().st_size if item.is_file() else 0,
|
||||
"modified": datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
||||
})
|
||||
return tree
|
||||
|
||||
def get_file_content(file_path):
|
||||
"""获取文件内容"""
|
||||
full_path = CODE_DIR / file_path
|
||||
if not full_path.exists() or full_path.is_dir():
|
||||
return None
|
||||
|
||||
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
return f.read()
|
||||
|
||||
def get_file_metadata(file_path):
|
||||
"""获取文件元数据"""
|
||||
full_path = CODE_DIR / file_path
|
||||
if not full_path.exists():
|
||||
return None
|
||||
|
||||
# 使用正斜杠统一路径分隔符
|
||||
path_str = str(full_path.relative_to(CODE_DIR)).replace('\\', '/')
|
||||
|
||||
return {
|
||||
"name": full_path.name,
|
||||
"path": path_str,
|
||||
"size": full_path.stat().st_size,
|
||||
"modified": datetime.fromtimestamp(full_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"extension": full_path.suffix.lower()
|
||||
}
|
||||
|
||||
def render_markdown(content):
|
||||
"""渲染Markdown为HTML"""
|
||||
extensions = ['extra', 'codehilite', 'toc', 'nl2br']
|
||||
return markdown.markdown(content, extensions=extensions)
|
||||
|
||||
def syntax_highlight(code, language=None):
|
||||
"""语法高亮"""
|
||||
if not language:
|
||||
try:
|
||||
lexer = guess_lexer(code)
|
||||
except:
|
||||
lexer = get_lexer_by_name("text")
|
||||
else:
|
||||
try:
|
||||
lexer = get_lexer_by_name(language)
|
||||
except:
|
||||
lexer = get_lexer_by_name("text")
|
||||
|
||||
formatter = HtmlFormatter(style="monokai", linenos=True)
|
||||
return highlight(code, lexer, formatter)
|
||||
|
||||
def create_zip_archive(branch, version):
|
||||
"""创建ZIP压缩包"""
|
||||
source_dir = CODE_DIR / branch / version
|
||||
if not source_dir.exists():
|
||||
return None
|
||||
|
||||
zip_name = f"{branch}_{version}.zip"
|
||||
zip_path = UPLOAD_DIR / zip_name
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for file_path in source_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
arcname = file_path.relative_to(source_dir)
|
||||
zf.write(file_path, arcname)
|
||||
|
||||
return zip_path
|
||||
|
||||
def create_tar_archive(branch, version):
|
||||
"""创建TAR.GZ压缩包"""
|
||||
source_dir = CODE_DIR / branch / version
|
||||
if not source_dir.exists():
|
||||
return None
|
||||
|
||||
tar_name = f"{branch}_{version}.tar.gz"
|
||||
tar_path = UPLOAD_DIR / tar_name
|
||||
|
||||
with tarfile.open(tar_path, 'w:gz') as tf:
|
||||
tf.add(source_dir, arcname=version)
|
||||
|
||||
return tar_path
|
||||
|
||||
def find_whl_file(version):
|
||||
"""查找对应版本的whl文件"""
|
||||
for whl_file in PYPI_DIR.glob(f"*{version}*.whl"):
|
||||
return whl_file
|
||||
return None
|
||||
|
||||
# ============================================
|
||||
# 管理员认证装饰器
|
||||
# ============================================
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('admin_logged_in'):
|
||||
return redirect(url_for('admin_login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# ============================================
|
||||
# 路由 - 首页
|
||||
# ============================================
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页"""
|
||||
branches_data = get_branches_and_versions()
|
||||
|
||||
# 获取最新发行版
|
||||
releases = []
|
||||
for branch, versions in branches_data.items():
|
||||
for version in versions[:1]: # 每个分支最新版本
|
||||
whl_file = find_whl_file(version['name'])
|
||||
releases.append({
|
||||
"branch": branch,
|
||||
"version": version['name'],
|
||||
"date": version['created'],
|
||||
"has_whl": whl_file is not None
|
||||
})
|
||||
|
||||
# 获取公告内容
|
||||
announcement = None
|
||||
for branch, versions in branches_data.items():
|
||||
for version in versions:
|
||||
ann_path = CODE_DIR / branch / version['name'] / "ANNCMNT.md"
|
||||
if ann_path.exists():
|
||||
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
announcement = render_markdown(f.read())
|
||||
break
|
||||
if announcement:
|
||||
break
|
||||
|
||||
return render_template('index.html',
|
||||
branches=branches_data,
|
||||
releases=releases[:5],
|
||||
announcement=announcement)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 代码浏览器
|
||||
# ============================================
|
||||
@app.route('/code')
|
||||
def code_browser():
|
||||
"""代码浏览器主页"""
|
||||
branch = request.args.get('branch', '')
|
||||
version = request.args.get('version', '')
|
||||
|
||||
branches_data = get_branches_and_versions()
|
||||
|
||||
# 如果没有指定分支,使用第一个
|
||||
if not branch and branches_data:
|
||||
branch = list(branches_data.keys())[0]
|
||||
if branches_data[branch]:
|
||||
version = branches_data[branch][0]['name']
|
||||
|
||||
file_tree = get_file_tree(branch, version) if branch and version else []
|
||||
|
||||
return render_template('code_browser.html',
|
||||
branches=branches_data,
|
||||
current_branch=branch,
|
||||
current_version=version,
|
||||
file_tree=file_tree)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 目录浏览
|
||||
# ============================================
|
||||
@app.route('/browse/<path:dir_path>')
|
||||
def browse_directory(dir_path):
|
||||
"""浏览子目录"""
|
||||
branch = request.args.get('branch', '')
|
||||
version = request.args.get('version', '')
|
||||
|
||||
full_dir = CODE_DIR / dir_path
|
||||
if not full_dir.exists() or not full_dir.is_dir():
|
||||
abort(404)
|
||||
|
||||
files = []
|
||||
for item in sorted(full_dir.iterdir()):
|
||||
files.append({
|
||||
"name": item.name,
|
||||
"type": "dir" if item.is_dir() else "file",
|
||||
"path": str(item.relative_to(CODE_DIR)).replace('\\', '/'),
|
||||
"size": item.stat().st_size if item.is_file() else 0,
|
||||
"modified": datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
||||
})
|
||||
|
||||
# 面包屑导航
|
||||
breadcrumbs = []
|
||||
parts = Path(dir_path).parts
|
||||
current = ""
|
||||
for part in parts:
|
||||
current = str(Path(current) / part) if current else part
|
||||
breadcrumbs.append({"name": part, "path": current.replace('\\', '/')})
|
||||
|
||||
return render_template('directory.html',
|
||||
files=files,
|
||||
current_path=dir_path,
|
||||
breadcrumbs=breadcrumbs,
|
||||
branch=branch,
|
||||
version=version)
|
||||
|
||||
@app.route('/view/<path:file_path>')
|
||||
def view_file(file_path):
|
||||
"""查看文件内容"""
|
||||
branch = request.args.get('branch', '')
|
||||
version = request.args.get('version', '')
|
||||
|
||||
# 将反斜杠替换为正斜杠
|
||||
file_path = file_path.replace('\\', '/')
|
||||
|
||||
# 构建完整路径
|
||||
if branch and version:
|
||||
# 如果 file_path 已经包含分支,直接使用
|
||||
if file_path.startswith(f"{branch}/"):
|
||||
full_path = CODE_DIR / file_path
|
||||
else:
|
||||
# 否则拼接分支和版本
|
||||
full_path = CODE_DIR / branch / version / file_path
|
||||
else:
|
||||
full_path = CODE_DIR / file_path
|
||||
|
||||
if not full_path.exists() or full_path.is_dir():
|
||||
abort(404)
|
||||
|
||||
metadata = get_file_metadata(str(full_path.relative_to(CODE_DIR)))
|
||||
extension = full_path.suffix.lower()
|
||||
|
||||
# Python文件 - 语法高亮
|
||||
if extension == '.py':
|
||||
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
|
||||
highlighted = syntax_highlight(content, 'python')
|
||||
return render_template('view_code.html',
|
||||
metadata=metadata,
|
||||
content=content,
|
||||
highlighted_code=highlighted,
|
||||
language='python',
|
||||
branch=branch,
|
||||
version=version)
|
||||
|
||||
# Markdown文件 - 渲染
|
||||
elif extension in ['.md', '.markdown']:
|
||||
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
|
||||
html_content = render_markdown(content)
|
||||
return render_template('view_markdown.html',
|
||||
metadata=metadata,
|
||||
html_content=Markup(html_content),
|
||||
branch=branch,
|
||||
version=version)
|
||||
|
||||
# 文本文件
|
||||
elif extension in ['.txt', '.json', '.xml', '.yaml', '.yml', '.conf', '.cfg', '.ini']:
|
||||
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
|
||||
return render_template('view_file.html',
|
||||
metadata=metadata,
|
||||
content=content,
|
||||
is_text=True,
|
||||
branch=branch,
|
||||
version=version)
|
||||
|
||||
# 其他文件 - 显示下载选项
|
||||
else:
|
||||
return render_template('view_file.html',
|
||||
metadata=metadata,
|
||||
is_text=False,
|
||||
branch=branch,
|
||||
version=version)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 下载文件
|
||||
# ============================================
|
||||
@app.route('/download/<path:file_path>')
|
||||
def download_file(file_path):
|
||||
"""下载单个文件"""
|
||||
file_path = file_path.replace('\\', '/')
|
||||
full_path = CODE_DIR / file_path
|
||||
if not full_path.exists() or full_path.is_dir():
|
||||
abort(404)
|
||||
|
||||
return send_file(full_path, as_attachment=True)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 下载压缩包
|
||||
# ============================================
|
||||
@app.route('/download/zip/<branch>/<version>')
|
||||
def download_zip(branch, version):
|
||||
"""下载ZIP压缩包"""
|
||||
zip_path = create_zip_archive(branch, version)
|
||||
if not zip_path:
|
||||
abort(404)
|
||||
|
||||
return send_file(zip_path, as_attachment=True, download_name=f"{branch}_{version}.zip")
|
||||
|
||||
@app.route('/download/tar/<branch>/<version>')
|
||||
def download_tar(branch, version):
|
||||
"""下载TAR.GZ压缩包"""
|
||||
tar_path = create_tar_archive(branch, version)
|
||||
if not tar_path:
|
||||
abort(404)
|
||||
|
||||
return send_file(tar_path, as_attachment=True, download_name=f"{branch}_{version}.tar.gz")
|
||||
|
||||
@app.route('/download/whl/<version>')
|
||||
def download_whl(version):
|
||||
"""下载whl文件 - 支持带v和不带v的版本号"""
|
||||
# 去除版本号开头的 'v' 如果存在
|
||||
clean_version = version.lstrip('v')
|
||||
|
||||
# 先尝试用原始版本号查找
|
||||
whl_file = find_whl_file(version)
|
||||
|
||||
# 如果没找到,尝试用去掉v的版本号查找
|
||||
if not whl_file and clean_version != version:
|
||||
whl_file = find_whl_file(clean_version)
|
||||
|
||||
# 如果还没找到,尝试模糊匹配(直接搜索包含版本号的whl文件)
|
||||
if not whl_file:
|
||||
for whl in PYPI_DIR.glob(f"*{clean_version}*.whl"):
|
||||
whl_file = whl
|
||||
break
|
||||
|
||||
if not whl_file or not whl_file.exists():
|
||||
abort(404)
|
||||
|
||||
return send_file(whl_file, as_attachment=True, download_name=whl_file.name)
|
||||
|
||||
@app.route('/api/version-info')
|
||||
def api_version_info():
|
||||
"""获取版本信息"""
|
||||
return jsonify({
|
||||
"version": "v3.0.0 LTS",
|
||||
"title": "轻量级 Python 音频革命",
|
||||
"subtitle": "2.5MB 终结 160MB 臃肿时代",
|
||||
"size": "2.5MB",
|
||||
"description": "极轻量级 Python 音频库,完整元数据解析,非阻塞播放。首个长期支持版本,永久免费技术支持。"
|
||||
})
|
||||
|
||||
@app.route('/api/latest-announcement')
|
||||
def api_latest_announcement():
|
||||
"""获取最新公告(ANNCMNT.md)"""
|
||||
# 查找最新的 ANNCMNT.md
|
||||
branches_data = get_branches_and_versions()
|
||||
announcement_html = None
|
||||
|
||||
for branch, versions in branches_data.items():
|
||||
for version in versions[:1]:
|
||||
ann_path = CODE_DIR / branch / version['name'] / "ANNCMNT.md"
|
||||
if ann_path.exists():
|
||||
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
announcement_html = render_markdown(content)
|
||||
break
|
||||
if announcement_html:
|
||||
break
|
||||
|
||||
return jsonify({"html": announcement_html})
|
||||
|
||||
# ============================================
|
||||
# 路由 - 发行版页面
|
||||
# ============================================
|
||||
@app.route('/releases')
|
||||
def releases():
|
||||
"""发行版列表页"""
|
||||
branches_data = get_branches_and_versions()
|
||||
|
||||
all_releases = []
|
||||
for branch, versions in branches_data.items():
|
||||
for version in versions:
|
||||
whl_file = find_whl_file(version['name'])
|
||||
# 获取该版本的公告内容
|
||||
announcement = get_version_announcement(branch, version['name'])
|
||||
all_releases.append({
|
||||
"branch": branch,
|
||||
"version": version['name'],
|
||||
"date": version['created'],
|
||||
"has_whl": whl_file is not None,
|
||||
"announcement": announcement
|
||||
})
|
||||
|
||||
# 按版本号排序(降序)
|
||||
all_releases.sort(key=lambda x: x['version'], reverse=True)
|
||||
|
||||
return render_template('releases.html', releases=all_releases)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 搜索
|
||||
# ============================================
|
||||
@app.route('/search')
|
||||
def search():
|
||||
"""全目录搜索"""
|
||||
query = request.args.get('q', '').strip()
|
||||
branch = request.args.get('branch', '')
|
||||
version = request.args.get('version', '')
|
||||
|
||||
results = []
|
||||
if query and branch and version:
|
||||
search_dir = CODE_DIR / branch / version
|
||||
if search_dir.exists():
|
||||
for file_path in search_dir.rglob("*"):
|
||||
if file_path.is_file() and query.lower() in file_path.name.lower():
|
||||
results.append({
|
||||
"name": file_path.name,
|
||||
"path": str(file_path.relative_to(CODE_DIR)).replace('\\', '/'),
|
||||
"size": file_path.stat().st_size,
|
||||
"modified": datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
||||
})
|
||||
|
||||
return render_template('code_browser.html',
|
||||
branches=get_branches_and_versions(),
|
||||
current_branch=branch,
|
||||
current_version=version,
|
||||
search_query=query,
|
||||
search_results=results)
|
||||
|
||||
# ============================================
|
||||
# 路由 - 反馈
|
||||
# ============================================
|
||||
@app.route('/feedback', methods=['GET', 'POST'])
|
||||
def feedback():
|
||||
"""反馈页面"""
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email', '').strip()
|
||||
content = request.form.get('content', '').strip()
|
||||
|
||||
if not email or not content:
|
||||
return render_template('feedback.html', error="请填写邮箱和反馈内容")
|
||||
|
||||
# 保存反馈
|
||||
feedbacks = load_feedbacks()
|
||||
feedbacks.append({
|
||||
"id": len(feedbacks) + 1,
|
||||
"email": email,
|
||||
"content": content,
|
||||
"status": "unread",
|
||||
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
save_feedbacks(feedbacks)
|
||||
|
||||
# 发送邮件
|
||||
send_feedback_email(email, content)
|
||||
|
||||
return render_template('feedback.html', success="反馈已提交,感谢您的建议!")
|
||||
|
||||
return render_template('feedback.html')
|
||||
|
||||
# ============================================
|
||||
# 路由 - API
|
||||
# ============================================
|
||||
@app.route('/api/download')
|
||||
def api_download():
|
||||
"""API获取下载地址"""
|
||||
branch = request.args.get('branch', '')
|
||||
version = request.args.get('version', '')
|
||||
|
||||
if not branch or not version:
|
||||
return jsonify({"error": "缺少 branch 或 version 参数"}), 400
|
||||
|
||||
# 检查版本是否存在
|
||||
version_path = CODE_DIR / branch / version
|
||||
if not version_path.exists():
|
||||
return jsonify({"error": "版本不存在"}), 404
|
||||
|
||||
base_url = request.host_url.rstrip('/')
|
||||
|
||||
response = {
|
||||
"branch": branch,
|
||||
"version": version,
|
||||
"downloads": {
|
||||
"zip": f"{base_url}/download/zip/{branch}/{version}",
|
||||
"tar": f"{base_url}/download/tar/{branch}/{version}"
|
||||
}
|
||||
}
|
||||
|
||||
whl_file = find_whl_file(version)
|
||||
if whl_file:
|
||||
response["downloads"]["whl"] = f"{base_url}/download/whl/{version}"
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
# ============================================
|
||||
# 管理员路由
|
||||
# ============================================
|
||||
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||
def admin_login():
|
||||
"""管理员登录"""
|
||||
if request.method == 'POST':
|
||||
password = request.form.get('password', '')
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
if password_hash == ADMIN_PASSWORD_HASH:
|
||||
session['admin_logged_in'] = True
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
else:
|
||||
return render_template('admin_login.html', error="密码错误")
|
||||
|
||||
return render_template('admin_login.html')
|
||||
|
||||
@app.route('/admin/logout')
|
||||
def admin_logout():
|
||||
"""管理员登出"""
|
||||
session.pop('admin_logged_in', None)
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
@app.route('/admin')
|
||||
@admin_required
|
||||
def admin_dashboard():
|
||||
"""管理仪表板"""
|
||||
branches_data = get_branches_and_versions()
|
||||
feedbacks = load_feedbacks()
|
||||
|
||||
stats = {
|
||||
"branches": len(branches_data),
|
||||
"versions": sum(len(v) for v in branches_data.values()),
|
||||
"feedbacks": len(feedbacks),
|
||||
"unread_feedbacks": len([f for f in feedbacks if f.get('status') == 'unread'])
|
||||
}
|
||||
|
||||
return render_template('admin_dashboard.html', stats=stats, recent_feedbacks=feedbacks[-10:])
|
||||
|
||||
@app.route('/admin/feedbacks')
|
||||
@admin_required
|
||||
def admin_feedbacks():
|
||||
"""反馈管理"""
|
||||
feedbacks = load_feedbacks()
|
||||
return render_template('admin_feedbacks.html', feedbacks=feedbacks)
|
||||
|
||||
@app.route('/admin/feedback/mark_read/<int:feedback_id>')
|
||||
@admin_required
|
||||
def admin_feedback_mark_read(feedback_id):
|
||||
"""标记反馈为已读"""
|
||||
feedbacks = load_feedbacks()
|
||||
for f in feedbacks:
|
||||
if f.get('id') == feedback_id:
|
||||
f['status'] = 'read'
|
||||
break
|
||||
save_feedbacks(feedbacks)
|
||||
return redirect(url_for('admin_feedbacks'))
|
||||
|
||||
@app.route('/admin/feedback/delete/<int:feedback_id>')
|
||||
@admin_required
|
||||
def admin_feedback_delete(feedback_id):
|
||||
"""删除反馈"""
|
||||
feedbacks = load_feedbacks()
|
||||
feedbacks = [f for f in feedbacks if f.get('id') != feedback_id]
|
||||
save_feedbacks(feedbacks)
|
||||
return redirect(url_for('admin_feedbacks'))
|
||||
|
||||
@app.route('/admin/branches')
|
||||
@admin_required
|
||||
def admin_branches():
|
||||
"""分支管理"""
|
||||
branches_data = get_branches_and_versions()
|
||||
return render_template('admin_branches.html', branches=branches_data)
|
||||
|
||||
@app.route('/admin/branch/create', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_branch_create():
|
||||
"""创建分支"""
|
||||
branch_name = request.form.get('branch_name', '').strip()
|
||||
if not branch_name:
|
||||
return jsonify({"error": "分支名不能为空"}), 400
|
||||
|
||||
# 安全处理分支名
|
||||
branch_name = secure_filename(branch_name)
|
||||
branch_path = CODE_DIR / branch_name
|
||||
|
||||
if branch_path.exists():
|
||||
return jsonify({"error": "分支已存在"}), 400
|
||||
|
||||
branch_path.mkdir(parents=True)
|
||||
|
||||
# 更新分支数据
|
||||
branches = load_branches()
|
||||
branches[branch_name] = []
|
||||
save_branches(branches)
|
||||
|
||||
return redirect(url_for('admin_branches'))
|
||||
|
||||
@app.route('/admin/version/upload', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_version_upload():
|
||||
"""上传新版本"""
|
||||
branch = request.form.get('branch', '').strip()
|
||||
version = request.form.get('version', '').strip()
|
||||
|
||||
if not branch or not version:
|
||||
return jsonify({"error": "分支和版本不能为空"}), 400
|
||||
|
||||
version = secure_filename(version)
|
||||
version_path = CODE_DIR / branch / version
|
||||
|
||||
if version_path.exists():
|
||||
return jsonify({"error": "版本已存在"}), 400
|
||||
|
||||
version_path.mkdir(parents=True)
|
||||
|
||||
# 处理文件上传
|
||||
if 'files' in request.files:
|
||||
files = request.files.getlist('files')
|
||||
for file in files:
|
||||
if file and file.filename:
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(version_path / filename)
|
||||
|
||||
# 更新分支数据
|
||||
branches = load_branches()
|
||||
if branch not in branches:
|
||||
branches[branch] = []
|
||||
if version not in branches[branch]:
|
||||
branches[branch].append(version)
|
||||
save_branches(branches)
|
||||
|
||||
return redirect(url_for('admin_branches'))
|
||||
|
||||
@app.route('/admin/version/delete/<branch>/<version>')
|
||||
@admin_required
|
||||
def admin_version_delete(branch, version):
|
||||
"""删除版本"""
|
||||
version_path = CODE_DIR / branch / version
|
||||
if version_path.exists():
|
||||
shutil.rmtree(version_path)
|
||||
|
||||
# 更新分支数据
|
||||
branches = load_branches()
|
||||
if branch in branches and version in branches[branch]:
|
||||
branches[branch].remove(version)
|
||||
save_branches(branches)
|
||||
|
||||
return redirect(url_for('admin_branches'))
|
||||
|
||||
@app.route('/admin/whl/upload', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_whl_upload():
|
||||
"""上传whl文件"""
|
||||
if 'whl_file' not in request.files:
|
||||
return jsonify({"error": "请选择文件"}), 400
|
||||
|
||||
file = request.files['whl_file']
|
||||
if file.filename == '':
|
||||
return jsonify({"error": "请选择文件"}), 400
|
||||
|
||||
if file.filename.endswith('.whl'):
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(PYPI_DIR / filename)
|
||||
|
||||
return redirect(url_for('admin_dashboard'))
|
||||
|
||||
# ============================================
|
||||
# 错误页面
|
||||
# ============================================
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
# ============================================
|
||||
# 主程序入口
|
||||
# ============================================
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("ap_ds 代码仓库系统启动")
|
||||
print("访问地址: http://127.0.0.1:8888")
|
||||
print("管理员后台: http://127.0.0.1:8888/admin/login")
|
||||
print("=" * 50)
|
||||
app.run(host='0.0.0.0', port=8888, debug=True)
|
||||
+2312
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - 页面未找到{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container repo-text-center repo-py-10">
|
||||
<span style="font-size: 6rem;">🔍</span>
|
||||
<h1 class="repo-text-2xl repo-font-bold repo-mt-4">404</h1>
|
||||
<p class="repo-text-secondary repo-mt-2">页面未找到</p>
|
||||
<a href="{{ url_for('index') }}" class="repo-btn repo-btn--primary repo-mt-6">返回首页</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}500 - 服务器错误{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container repo-text-center repo-py-10">
|
||||
<span style="font-size: 6rem;">⚠️</span>
|
||||
<h1 class="repo-text-2xl repo-font-bold repo-mt-4">500</h1>
|
||||
<p class="repo-text-secondary repo-mt-2">服务器内部错误</p>
|
||||
<a href="{{ url_for('index') }}" class="repo-btn repo-btn--primary repo-mt-6">返回首页</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}分支管理 - ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container">
|
||||
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
|
||||
<h1 class="repo-text-2xl repo-font-bold">分支与版本管理</h1>
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="repo-btn repo-btn--ghost">返回仪表板</a>
|
||||
</div>
|
||||
|
||||
<!-- 创建分支 -->
|
||||
<div class="repo-card repo-mb-6">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">创建新分支</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
<form method="post" action="{{ url_for('admin_branch_create') }}" class="repo-flex repo-gap-3">
|
||||
<input type="text" name="branch_name" class="repo-input" placeholder="分支名称 (如: LFV)" required>
|
||||
<button type="submit" class="repo-btn repo-btn--primary">创建分支</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传新版本 -->
|
||||
<div class="repo-card repo-mb-6">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">上传新版本</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
<form method="post" action="{{ url_for('admin_version_upload') }}" enctype="multipart/form-data">
|
||||
<div class="repo-row">
|
||||
<div class="repo-col">
|
||||
<select name="branch" class="repo-select__trigger repo-w-full" required>
|
||||
<option value="">选择分支</option>
|
||||
{% for branch_name in branches.keys() %}
|
||||
<option value="{{ branch_name }}">{{ branch_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<input type="text" name="version" class="repo-input" placeholder="版本号 (如: v2.5.0)" required>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<input type="file" name="files" class="repo-input" multiple>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<button type="submit" class="repo-btn repo-btn--primary">上传版本</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分支列表 -->
|
||||
{% for branch_name, versions in branches.items() %}
|
||||
<div class="repo-card repo-mb-6">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">📁 {{ branch_name }}</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
<table class="repo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in versions %}
|
||||
<tr>
|
||||
<td>{{ version.name }}</td>
|
||||
<td class="repo-text-secondary">{{ version.created }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('admin_version_delete', branch=branch_name, version=version.name) }}"
|
||||
class="repo-btn repo-btn--sm repo-btn--danger"
|
||||
onclick="return confirm('确定要删除版本 {{ version.name }} 吗?')">
|
||||
删除
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}管理仪表板 - ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container">
|
||||
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
|
||||
<h1 class="repo-text-2xl repo-font-bold">管理仪表板</h1>
|
||||
<a href="{{ url_for('admin_logout') }}" class="repo-btn repo-btn--danger">登出</a>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="repo-row repo-mb-6">
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body repo-text-center">
|
||||
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-primary);">{{ stats.branches }}</div>
|
||||
<div class="repo-text-secondary">分支</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body repo-text-center">
|
||||
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-success);">{{ stats.versions }}</div>
|
||||
<div class="repo-text-secondary">版本</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body repo-text-center">
|
||||
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-warning);">{{ stats.feedbacks }}</div>
|
||||
<div class="repo-text-secondary">反馈总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body repo-text-center">
|
||||
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-danger);">{{ stats.unread_feedbacks }}</div>
|
||||
<div class="repo-text-secondary">未读反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="repo-row">
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">上传 WHL 文件</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
<form method="post" action="{{ url_for('admin_whl_upload') }}" enctype="multipart/form-data">
|
||||
<input type="file" name="whl_file" accept=".whl" class="repo-input repo-mb-3" required>
|
||||
<button type="submit" class="repo-btn repo-btn--primary">上传</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-col">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">管理链接</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
<div class="repo-flex repo-gap-3">
|
||||
<a href="{{ url_for('admin_branches') }}" class="repo-btn repo-btn--primary">分支管理</a>
|
||||
<a href="{{ url_for('admin_feedbacks') }}" class="repo-btn repo-btn--secondary">反馈管理</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近反馈 -->
|
||||
<div class="repo-card repo-mt-6">
|
||||
<div class="repo-card__header">
|
||||
<h3 class="repo-card__title">最近反馈</h3>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
{% if recent_feedbacks %}
|
||||
<table class="repo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>邮箱</th>
|
||||
<th>内容</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in recent_feedbacks %}
|
||||
<tr>
|
||||
<td class="repo-text-sm">{{ f.created }}</td>
|
||||
<td class="repo-text-sm">{{ f.email }}</td>
|
||||
<td class="repo-text-sm repo-truncate" style="max-width: 300px;">{{ f.content[:50] }}...</td>
|
||||
<td>
|
||||
{% if f.status == 'unread' %}
|
||||
<span class="repo-tag repo-tag--danger">未读</span>
|
||||
{% else %}
|
||||
<span class="repo-tag repo-tag--default">已读</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="repo-text-secondary repo-text-center">暂无反馈</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}反馈管理 - ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container">
|
||||
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
|
||||
<h1 class="repo-text-2xl repo-font-bold">反馈管理</h1>
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="repo-btn repo-btn--ghost">返回仪表板</a>
|
||||
</div>
|
||||
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body">
|
||||
{% if feedbacks %}
|
||||
<table class="repo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>时间</th>
|
||||
<th>邮箱</th>
|
||||
<th>内容</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in feedbacks|reverse %}
|
||||
<tr>
|
||||
<td>{{ f.id }}</td>
|
||||
<td class="repo-text-sm">{{ f.created }}</td>
|
||||
<td class="repo-text-sm">{{ f.email }}</td>
|
||||
<td class="repo-text-sm" style="max-width: 400px;">
|
||||
<div class="repo-line-clamp-2">{{ f.content }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if f.status == 'unread' %}
|
||||
<span class="repo-tag repo-tag--danger">未读</span>
|
||||
{% else %}
|
||||
<span class="repo-tag repo-tag--default">已读</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="repo-flex repo-gap-2">
|
||||
{% if f.status == 'unread' %}
|
||||
<a href="{{ url_for('admin_feedback_mark_read', feedback_id=f.id) }}" class="repo-btn repo-btn--sm repo-btn--primary">标记已读</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin_feedback_delete', feedback_id=f.id) }}"
|
||||
class="repo-btn repo-btn--sm repo-btn--danger"
|
||||
onclick="return confirm('确定要删除这条反馈吗?')">删除</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="repo-text-secondary repo-text-center">暂无反馈</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理员登录 - ap_ds</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css.css') }}">
|
||||
</head>
|
||||
<body style="background: var(--repo-color-bg-secondary); min-height: 100vh; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="repo-card" style="width: 100%; max-width: 400px;">
|
||||
<div class="repo-card__header">
|
||||
<h2 class="repo-card__title">管理员登录</h2>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
{% if error %}
|
||||
<div class="repo-message repo-message--error" style="position: static; transform: none; margin-bottom: 1rem;">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<div class="repo-mb-4">
|
||||
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">密码</label>
|
||||
<input type="password" name="password" class="repo-input" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="repo-btn repo-btn--primary repo-w-full">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,581 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}ap_ds · 轻量级Python音频库{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css.css') }}">
|
||||
<!-- highlight.js 主题 (浅色) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
/* 浅色主题覆盖 - 基于 css.css 变量 */
|
||||
:root {
|
||||
--repo-color-primary: #3b82f6;
|
||||
--repo-color-primary-hover: #2563eb;
|
||||
--repo-color-text: #1f2937;
|
||||
--repo-color-text-secondary: #4b5563;
|
||||
--repo-color-text-tertiary: #9ca3af;
|
||||
--repo-color-bg: #ffffff;
|
||||
--repo-color-bg-secondary: #f9fafb;
|
||||
--repo-color-bg-tertiary: #f3f4f6;
|
||||
--repo-color-border: #e5e7eb;
|
||||
--repo-color-border-hover: #d1d5db;
|
||||
}
|
||||
|
||||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--repo-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif);
|
||||
font-size: var(--repo-font-size-base, 14px);
|
||||
line-height: var(--repo-line-height-base, 1.5);
|
||||
color: var(--repo-color-text);
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
}
|
||||
|
||||
/* 导航栏 - 使用 css.css 变量 */
|
||||
.header {
|
||||
background-color: var(--repo-color-bg);
|
||||
border-bottom: 1px solid var(--repo-color-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--repo-z-sticky, 100);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--repo-spacing-6, 16px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--repo-header-height, 60px);
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: var(--repo-font-size-xl, 18px);
|
||||
font-weight: 600;
|
||||
color: var(--repo-color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
color: var(--repo-color-primary);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--repo-color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--repo-font-size-base, 14px);
|
||||
font-weight: 500;
|
||||
padding: var(--repo-spacing-2, 8px) 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--repo-color-primary);
|
||||
}
|
||||
|
||||
.repo-badge {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--repo-radius-full, 20px);
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
font-family: var(--repo-font-family-mono, monospace);
|
||||
color: var(--repo-color-text-secondary);
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: var(--repo-spacing-6, 24px) var(--repo-spacing-4, 16px);
|
||||
}
|
||||
|
||||
/* 仓库头部 */
|
||||
.repo-header {
|
||||
margin-bottom: var(--repo-spacing-6, 24px);
|
||||
}
|
||||
|
||||
.repo-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--repo-spacing-2, 8px);
|
||||
font-size: var(--repo-font-size-xl, 18px);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
|
||||
.repo-path a {
|
||||
color: var(--repo-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.repo-path a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.repo-path .separator {
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
/* 两栏布局 */
|
||||
.two-column {
|
||||
display: flex;
|
||||
gap: var(--repo-spacing-8, 32px);
|
||||
}
|
||||
|
||||
/* 左侧边栏 */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-bottom: var(--repo-spacing-6, 24px);
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
font-weight: 600;
|
||||
color: var(--repo-color-text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: var(--repo-spacing-3, 12px);
|
||||
}
|
||||
|
||||
.branch-selector {
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.branch-selector:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
border-color: var(--repo-color-border-hover);
|
||||
}
|
||||
|
||||
/* 右侧主内容 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 文件列表表格 */
|
||||
.file-list {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
|
||||
border-bottom: 1px solid var(--repo-color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-4, 16px);
|
||||
border-bottom: 1px solid var(--repo-color-border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-row:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
}
|
||||
|
||||
.file-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--repo-spacing-2, 8px);
|
||||
}
|
||||
|
||||
.file-name a {
|
||||
color: var(--repo-color-primary);
|
||||
text-decoration: none;
|
||||
font-family: var(--repo-font-family-mono, monospace);
|
||||
font-size: var(--repo-font-size-sm, 13px);
|
||||
}
|
||||
|
||||
.file-name a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-message {
|
||||
flex: 2;
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
.file-time {
|
||||
width: 100px;
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* README 区域 */
|
||||
.readme-section {
|
||||
margin-top: var(--repo-spacing-8, 32px);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.readme-header {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
|
||||
border-bottom: 1px solid var(--repo-color-border);
|
||||
font-weight: 600;
|
||||
font-size: var(--repo-font-size-base, 14px);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.readme-content {
|
||||
padding: var(--repo-spacing-8, 32px);
|
||||
background-color: var(--repo-color-bg);
|
||||
}
|
||||
|
||||
.readme-content h1, .readme-content h2, .readme-content h3 {
|
||||
margin-top: var(--repo-spacing-6, 24px);
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.readme-content h1 { font-size: 2em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
.readme-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
.readme-content h3 { font-size: 1.25em; }
|
||||
.readme-content p { margin-bottom: var(--repo-spacing-4, 16px); line-height: 1.6; }
|
||||
.readme-content code {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: var(--repo-radius-sm, 4px);
|
||||
font-family: var(--repo-font-family-mono, monospace);
|
||||
font-size: 85%;
|
||||
}
|
||||
.readme-content pre {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: var(--repo-spacing-4, 16px);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
overflow-x: auto;
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
.readme-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.readme-content ul, .readme-content ol {
|
||||
padding-left: 2em;
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
.readme-content blockquote {
|
||||
border-left: 4px solid var(--repo-color-border);
|
||||
padding-left: var(--repo-spacing-4, 16px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
.readme-content table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
width: 100%;
|
||||
}
|
||||
.readme-content th, .readme-content td {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
|
||||
}
|
||||
.readme-content th {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
}
|
||||
|
||||
/* 代码查看器 */
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px) 0 0;
|
||||
}
|
||||
|
||||
.code-content {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-content pre {
|
||||
margin: 0;
|
||||
padding: var(--repo-spacing-4, 16px);
|
||||
background-color: var(--repo-color-bg);
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
font-family: var(--repo-font-family-mono, "SF Mono", Monaco, monospace);
|
||||
font-size: var(--repo-font-size-sm, 13px);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 按钮 - 使用 css.css 样式 */
|
||||
.copy-btn {
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
color: var(--repo-color-text-secondary);
|
||||
padding: var(--repo-spacing-1, 4px) var(--repo-spacing-3, 12px);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
border-color: var(--repo-color-border-hover);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--repo-spacing-1, 4px);
|
||||
padding: var(--repo-spacing-1, 4px) var(--repo-spacing-3, 12px);
|
||||
font-size: var(--repo-font-size-xs, 12px);
|
||||
font-weight: 500;
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--repo-color-primary);
|
||||
border: 1px solid transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--repo-color-primary-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
border-color: var(--repo-color-border-hover);
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: var(--repo-spacing-2, 8px);
|
||||
margin-bottom: var(--repo-spacing-6, 24px);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
|
||||
color: var(--repo-color-text);
|
||||
font-size: var(--repo-font-size-base, 14px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--repo-color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.repo-card-custom {
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.two-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
.file-message {
|
||||
display: none;
|
||||
}
|
||||
.header-inner {
|
||||
padding: 0 var(--repo-spacing-4, 16px);
|
||||
}
|
||||
.nav-links {
|
||||
gap: var(--repo-spacing-3, 12px);
|
||||
}
|
||||
.nav-links a {
|
||||
font-size: var(--repo-font-size-sm, 13px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 图标占位 */
|
||||
.icon {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: var(--repo-spacing-8, 32px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.info-message {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
border-left: 4px solid var(--repo-color-primary);
|
||||
padding: var(--repo-spacing-4, 16px);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fef2f2;
|
||||
border-left: 4px solid var(--repo-color-danger, #ef4444);
|
||||
padding: var(--repo-spacing-4, 16px);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: #f0fdf4;
|
||||
border-left: 4px solid var(--repo-color-success, #10b981);
|
||||
padding: var(--repo-spacing-4, 16px);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
margin-bottom: var(--repo-spacing-4, 16px);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
/* 分支菜单下拉 */
|
||||
.branch-menu-dropdown {
|
||||
position: absolute;
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
width: 200px;
|
||||
z-index: var(--repo-z-dropdown, 100);
|
||||
box-shadow: var(--repo-shadow-lg, 0 10px 15px -3px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.branch-menu-item {
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.branch-menu-item:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-inner">
|
||||
<div class="logo-area">
|
||||
<a href="/" class="logo">ap_<span>ds</span></a>
|
||||
<div class="nav-links">
|
||||
<a href="{{ url_for('index') }}">首页</a>
|
||||
<a href="{{ url_for('code_browser') }}">代码</a>
|
||||
<a href="{{ url_for('feedback') }}">提问/建议</a>
|
||||
<a href="{{ url_for('releases') }}">发行版</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-badge">
|
||||
v3.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化 highlight.js
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('pre code').forEach(function(block) {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
});
|
||||
|
||||
// 复制代码功能
|
||||
function copyCode(btn) {
|
||||
var pre = btn.closest('.code-header')?.nextElementSibling;
|
||||
var codeBlock = pre?.querySelector('code');
|
||||
if (!codeBlock) return;
|
||||
var code = codeBlock.innerText;
|
||||
navigator.clipboard.writeText(code).then(function() {
|
||||
var originalText = btn.innerText;
|
||||
btn.innerText = '已复制!';
|
||||
setTimeout(function() { btn.innerText = originalText; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// 全局分支/版本选择辅助函数
|
||||
function selectBranch(branch, baseUrl) {
|
||||
window.location.href = baseUrl + '?branch=' + encodeURIComponent(branch);
|
||||
}
|
||||
|
||||
function selectVersion(version, baseUrl, branch) {
|
||||
window.location.href = baseUrl + '?branch=' + encodeURIComponent(branch) + '&version=' + encodeURIComponent(version);
|
||||
}
|
||||
</script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,381 @@
|
||||
|
||||
|
||||
<!-- 英文 SEO 标签 -->
|
||||
<meta name="description" content="ap_ds official code repository. Browse branches (APDSLTS001), tags (v3.0.0), view README/setup.py with syntax highlighting, search files, and download ZIP/TAR.GZ/WHL. Self-hosted, never offline.">
|
||||
<meta name="keywords" content="ap_ds source code, Python audio library code, browse repository, download WHL, self-hosted git, APDSLTS001, v3.0.0 source">
|
||||
<meta property="og:title" content="ap_ds Code Repository – Browse & Download Source">
|
||||
<meta property="og:description" content="Official self-hosted Git-style code browser for ap_ds. Syntax-highlighted Python files, Markdown rendering, multi-format downloads.">
|
||||
<meta property="og:type" content="software">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="ap_ds Code Repository">
|
||||
|
||||
<!-- 中文 SEO 标签 -->
|
||||
<meta name="description" lang="zh-CN" content="ap_ds 官方代码仓库。浏览分支(APDSLTS001)和版本(v3.0.0)下的所有源文件,语法高亮预览 README/setup.py,文件搜索,一键下载 ZIP/TAR.GZ/WHL。自主托管,永不掉线。">
|
||||
<meta name="keywords" lang="zh-CN" content="ap_ds 源代码, Python音频库代码, 浏览仓库, 下载WHL, 自建Git仓库, APDSLTS001, v3.0.0源码">
|
||||
<meta property="og:title" lang="zh-CN" content="ap_ds 代码仓库 – 浏览与下载源码">
|
||||
<meta property="og:description" lang="zh-CN" content="ap_ds 官方自建代码仓库。支持语法高亮的 Python 文件预览、Markdown 渲染、多格式源码下载。">{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% block title %}ap_ds Code Repository – Official Self-Hosted Git Source Code & Downloads{% endblock %}<!-- 仓库头部 -->
|
||||
<div class="repo-header">
|
||||
<div class="repo-path">
|
||||
<a href="/code">ap_ds</a>
|
||||
<span class="separator">/</span>
|
||||
<span>{{ current_branch if current_branch else '选择分支' }}</span>
|
||||
{% if current_version %}
|
||||
<span class="separator">/</span>
|
||||
<span>{{ current_version }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-box">
|
||||
<form action="{{ url_for('search') }}" method="get" style="display: flex; gap: 8px; width: 100%;">
|
||||
<input type="hidden" name="branch" value="{{ current_branch }}">
|
||||
<input type="hidden" name="version" value="{{ current_version }}">
|
||||
<input type="text" name="q" class="search-input" placeholder="查找文件..." value="{{ search_query or '' }}">
|
||||
<button type="submit" class="btn btn-primary">搜索</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="two-column">
|
||||
<!-- 左侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">📌 分支</div>
|
||||
<div class="branch-selector" id="branch-selector">
|
||||
<span>{{ current_branch if current_branch else '选择分支' }}</span>
|
||||
<span>▼</span>
|
||||
</div>
|
||||
<div id="branch-menu" class="branch-menu-dropdown" style="display: none;">
|
||||
{% for branch_name in branches.keys() %}
|
||||
<div class="branch-menu-item" data-branch="{{ branch_name }}">{{ branch_name }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">🏷️ 版本</div>
|
||||
<div class="branch-selector" id="version-selector">
|
||||
<span>{{ current_version if current_version else '选择版本' }}</span>
|
||||
<span>▼</span>
|
||||
</div>
|
||||
<div id="version-menu" class="branch-menu-dropdown" style="display: none;">
|
||||
{% if current_branch and branches[current_branch] %}
|
||||
{% for version in branches[current_branch] %}
|
||||
<div class="branch-menu-item" data-version="{{ version.name }}">{{ version.name }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">📦 下载</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<a href="{{ url_for('download_zip', branch=current_branch, version=current_version) if current_branch and current_version else '#' }}" class="btn btn-secondary {% if not current_branch or not current_version %}disabled{% endif %}" {% if not current_branch or not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
|
||||
📥 下载 ZIP
|
||||
</a>
|
||||
<a href="{{ url_for('download_tar', branch=current_branch, version=current_version) if current_branch and current_version else '#' }}" class="btn btn-secondary {% if not current_branch or not current_version %}disabled{% endif %}" {% if not current_branch or not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
|
||||
📥 下载 TAR.GZ
|
||||
</a>
|
||||
<a href="{{ url_for('download_whl', version=current_version) if current_version else '#' }}" class="btn btn-secondary {% if not current_version %}disabled{% endif %}" {% if not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
|
||||
📥 下载 WHL
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">🔗 快速链接</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||||
<a href="/releases" class="btn btn-secondary" style="text-align: center;">查看发行版</a>
|
||||
<a href="/feedback" class="btn btn-secondary" style="text-align: center;">提交反馈</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="main-content">
|
||||
<!-- 搜索结果 -->
|
||||
{% if search_results %}
|
||||
<div class="file-list">
|
||||
<div class="file-list-header">
|
||||
<span>🔍 搜索结果 ({{ search_results|length }})</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{% for result in search_results %}
|
||||
<div class="file-row">
|
||||
<div class="file-name">
|
||||
<span class="icon">📄</span>
|
||||
{% set relative_path = result.path.replace(current_branch + '/' + current_version + '/', '') %}
|
||||
<a href="{{ url_for('view_file', file_path=relative_path, branch=current_branch, version=current_version) }}">{{ result.name }}</a>
|
||||
</div>
|
||||
<div class="file-message">{{ result.path }}</div>
|
||||
<div class="file-time">
|
||||
{% if result.size < 1024 %}
|
||||
{{ result.size }} B
|
||||
{% elif result.size < 1024*1024 %}
|
||||
{{ (result.size/1024)|round(1) }} KB
|
||||
{% else %}
|
||||
{{ (result.size/(1024*1024))|round(1) }} MB
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 文件列表 -->
|
||||
{% if not search_results %}
|
||||
<div class="file-list">
|
||||
<div class="file-list-header">
|
||||
<span>📁 文件列表</span>
|
||||
<span></span>
|
||||
<span>大小</span>
|
||||
</div>
|
||||
<!-- 文件列表中的链接改为使用完整路径 -->
|
||||
{% for item in file_tree %}
|
||||
<div class="file-row">
|
||||
<div class="file-name">
|
||||
<span class="icon">{% if item.type == 'dir' %}📁{% else %}📄{% endif %}</span>
|
||||
{% if item.type == 'dir' %}
|
||||
<a href="{{ url_for('browse_directory', dir_path=item.path, branch=current_branch, version=current_version) }}">{{ item.name }}</a>
|
||||
{% else %}
|
||||
<!-- 直接使用完整路径,不加替换 -->
|
||||
<a href="{{ url_for('view_file', file_path=item.path, branch=current_branch, version=current_version) }}">{{ item.name }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="file-message">
|
||||
{% if item.type == 'dir' %}目录{% else %}{{ item.name }}{% endif %}
|
||||
</div>
|
||||
<div class="file-time">
|
||||
{% if item.type == 'file' %}
|
||||
{% if item.size < 1024 %}
|
||||
{{ item.size }} B
|
||||
{% elif item.size < 1024*1024 %}
|
||||
{{ (item.size/1024)|round(1) }} KB
|
||||
{% else %}
|
||||
{{ (item.size/(1024*1024))|round(1) }} MB
|
||||
{% endif %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="file-row">
|
||||
<div class="file-name" style="justify-content: center; color: var(--repo-color-text-tertiary);">
|
||||
此目录为空
|
||||
</div>
|
||||
<div class="file-message"></div>
|
||||
<div class="file-time"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- README 区域 -->
|
||||
<div class="readme-section">
|
||||
<div class="readme-header">
|
||||
📖 README.md
|
||||
</div>
|
||||
<div class="readme-content" id="readme-content">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取当前 URL 基础路径
|
||||
var currentUrl = "{{ url_for('code_browser') }}";
|
||||
var currentBranch = "{{ current_branch }}";
|
||||
var currentVersion = "{{ current_version }}";
|
||||
|
||||
// ========== 分支/版本选择器 ==========
|
||||
var branchSelector = document.getElementById('branch-selector');
|
||||
var branchMenu = document.getElementById('branch-menu');
|
||||
var versionSelector = document.getElementById('version-selector');
|
||||
var versionMenu = document.getElementById('version-menu');
|
||||
|
||||
if (branchSelector) {
|
||||
branchSelector.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
branchMenu.style.display = branchMenu.style.display === 'none' ? 'block' : 'none';
|
||||
if (versionMenu) versionMenu.style.display = 'none';
|
||||
};
|
||||
}
|
||||
|
||||
if (versionSelector) {
|
||||
versionSelector.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
versionMenu.style.display = versionMenu.style.display === 'none' ? 'block' : 'none';
|
||||
if (branchMenu) branchMenu.style.display = 'none';
|
||||
};
|
||||
}
|
||||
|
||||
document.onclick = function() {
|
||||
if (branchMenu) branchMenu.style.display = 'none';
|
||||
if (versionMenu) versionMenu.style.display = 'none';
|
||||
};
|
||||
|
||||
var branchItems = document.querySelectorAll('#branch-menu .branch-menu-item');
|
||||
branchItems.forEach(function(item) {
|
||||
item.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var branch = this.getAttribute('data-branch');
|
||||
if (branch) {
|
||||
window.location.href = currentUrl + '?branch=' + encodeURIComponent(branch);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var versionItems = document.querySelectorAll('#version-menu .branch-menu-item');
|
||||
versionItems.forEach(function(item) {
|
||||
item.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var version = this.getAttribute('data-version');
|
||||
if (version && currentBranch) {
|
||||
window.location.href = currentUrl + '?branch=' + encodeURIComponent(currentBranch) + '&version=' + encodeURIComponent(version);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 加载 README ==========
|
||||
function loadReadme() {
|
||||
if (!currentBranch || !currentVersion) {
|
||||
document.getElementById('readme-content').innerHTML = '<div class="info-message" style="text-align: center;">请选择分支和版本以查看 README</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var readmeUrl = '/view/' + currentBranch + '/' + currentVersion + '/README.md?branch=' + currentBranch + '&version=' + currentVersion;
|
||||
|
||||
fetch(readmeUrl)
|
||||
.then(function(res) {
|
||||
if (res.ok) {
|
||||
return res.text();
|
||||
}
|
||||
throw new Error('README.md 不存在');
|
||||
})
|
||||
.then(function(html) {
|
||||
var container = document.getElementById('readme-content');
|
||||
|
||||
if (html.indexOf('<div class="readme-content">') !== -1) {
|
||||
var match = html.match(/<div class="readme-content">([\s\S]*?)<\/div>/);
|
||||
if (match && match[1]) {
|
||||
container.innerHTML = match[1];
|
||||
} else {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
var codeBlocks = container.querySelectorAll('pre code');
|
||||
codeBlocks.forEach(function(block) {
|
||||
if (typeof hljs !== 'undefined') {
|
||||
hljs.highlightElement(block);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
document.getElementById('readme-content').innerHTML = '<div class="info-message" style="text-align: center;">📄 暂无 README.md 文件</div>';
|
||||
});
|
||||
}
|
||||
|
||||
loadReadme();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.branch-menu-dropdown {
|
||||
position: absolute;
|
||||
background-color: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
width: 200px;
|
||||
z-index: var(--repo-z-dropdown, 100);
|
||||
box-shadow: var(--repo-shadow-md);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.branch-menu-item {
|
||||
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.branch-menu-item:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: var(--repo-spacing-8, 32px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
.info-message {
|
||||
text-align: center;
|
||||
padding: var(--repo-spacing-8, 32px);
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
/* README 内容样式 */
|
||||
#readme-content h1, #readme-content h2, #readme-content h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
#readme-content h1 { font-size: 2em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
#readme-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
#readme-content p { margin-bottom: 16px; line-height: 1.6; }
|
||||
#readme-content code {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
#readme-content pre {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
#readme-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
#readme-content ul, #readme-content ol {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
#readme-content blockquote {
|
||||
border-left: 4px solid var(--repo-color-border);
|
||||
padding-left: 16px;
|
||||
color: var(--repo-color-text-tertiary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
#readme-content table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
#readme-content th, #readme-content td {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
#readme-content th {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}目录浏览 - ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="repo-breadcrumb repo-mb-4">
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-breadcrumb__link">根目录</a>
|
||||
{% for crumb in breadcrumbs %}
|
||||
<span class="repo-breadcrumb__separator">/</span>
|
||||
<a href="{{ url_for('browse_directory', dir_path=crumb.path, branch=branch, version=version) }}" class="repo-breadcrumb__link">
|
||||
{{ crumb.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body">
|
||||
<table class="repo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>大小</th>
|
||||
<th>修改时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if current_path %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-text-link">..</a>
|
||||
</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if file.type == 'dir' %}
|
||||
<span class="icon-folder repo-mr-2"></span>
|
||||
<a href="{{ url_for('browse_directory', dir_path=file.path, branch=branch, version=version) }}" class="repo-text-link">
|
||||
{{ file.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="icon-file repo-mr-2"></span>
|
||||
<a href="{{ url_for('view_file', file_path=file.path, branch=branch, version=version) }}" class="repo-text-link">
|
||||
{{ file.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="repo-text-secondary repo-text-sm">
|
||||
{% if file.type == 'file' %}
|
||||
{% if file.size < 1024 %}
|
||||
{{ file.size }} B
|
||||
{% elif file.size < 1024*1024 %}
|
||||
{{ (file.size/1024)|round(1) }} KB
|
||||
{% else %}
|
||||
{{ (file.size/(1024*1024))|round(1) }} MB
|
||||
{% endif %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="repo-text-secondary repo-text-sm">{{ file.modified }}</td>
|
||||
<td>
|
||||
{% if file.type == 'file' %}
|
||||
<a href="{{ url_for('download_file', file_path=file.path) }}" class="repo-btn repo-btn--sm repo-btn--ghost">下载</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
<!-- 英文 SEO 标签 -->
|
||||
<meta name="description" content="Official feedback channel for ap_ds. Submit bug reports, feature requests, or general inquiries via form. Includes FAQ: supported formats (MP3/FLAC/OGG/WAV), DAP playback logs. Direct email replies from maintainer.">
|
||||
<meta name="keywords" content="ap_ds feedback, bug report Python audio, feature request, ask question, DvsXT contact, MP3 FLAC OGG WAV support, Python audio library help">
|
||||
<meta property="og:title" content="ap_ds Feedback – Ask Questions & Report Issues">
|
||||
<meta property="og:description" content="Direct line to ap_ds maintainer. Submit feedback, get answers via email. Read FAQ before posting.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="ap_ds Feedback Form">
|
||||
|
||||
<!-- 中文 SEO 标签 -->
|
||||
<meta name="description" lang="zh-CN" content="ap_ds 官方反馈通道。提交 Bug 报告、功能建议或一般咨询。附常见问题:支持的音频格式(MP3/FLAC/OGG/WAV)、DAP 播放记录获取方法。维护者通过邮件直接回复。">
|
||||
<meta name="keywords" lang="zh-CN" content="ap_ds 反馈, 提交Bug, 功能建议, 提问, DvsXT 联系, MP3 FLAC OGG WAV支持, Python音频库帮助">
|
||||
<meta property="og:title" lang="zh-CN" content="ap_ds 反馈 – 提问与建议">
|
||||
<meta property="og:description" lang="zh-CN" content="直连 ap_ds 维护者。提交反馈,邮件获取回复。提交前可查阅常见问题。">{% extends "base.html" %}
|
||||
|
||||
{% block content %}{% block title %}ap_ds Feedback – Submit Questions, Bug Reports & Feature Requests{% endblock %}
|
||||
|
||||
<div class="repo-container" style="max-width: 800px;">
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__header">
|
||||
<h2 class="repo-card__title">💬 提问与建议</h2>
|
||||
<p class="repo-text-secondary repo-text-sm repo-mt-2">
|
||||
欢迎向我们提出任何问题或建议,我们会认真对待每一条反馈。
|
||||
</p>
|
||||
</div>
|
||||
<div class="repo-card__body">
|
||||
{% if error %}
|
||||
<div class="repo-message repo-message--error" style="position: static; transform: none; margin-bottom: 1rem;">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<div class="repo-message repo-message--success" style="position: static; transform: none; margin-bottom: 1rem;">
|
||||
{{ success }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<div class="repo-mb-4">
|
||||
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">邮箱地址</label>
|
||||
<input type="email" name="email" class="repo-input" required placeholder="your@email.com">
|
||||
<p class="repo-text-xs repo-text-tertiary repo-mt-1">我们会将回复发送到此邮箱</p>
|
||||
</div>
|
||||
<div class="repo-mb-4">
|
||||
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">反馈内容</label>
|
||||
<textarea name="content" class="repo-textarea repo-input" rows="8" required placeholder="请详细描述您的问题或建议..."></textarea>
|
||||
</div>
|
||||
<div class="repo-flex repo-gap-3">
|
||||
<button type="submit" class="repo-btn repo-btn--primary">提交反馈</button>
|
||||
<button type="reset" class="repo-btn repo-btn--secondary">清空</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-card repo-mt-6">
|
||||
<div class="repo-card__body">
|
||||
<h3 class="repo-font-semibold repo-mb-3">常见问题</h3>
|
||||
<div class="repo-space-y-3">
|
||||
<div>
|
||||
<div class="repo-font-medium">Q: ap_ds 支持哪些音频格式?</div>
|
||||
<p class="repo-text-secondary repo-text-sm">A: 支持 MP3、FLAC、OGG、WAV 四种主流格式。</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="repo-font-medium">Q: 遇到 SSL 证书错误怎么办?</div>
|
||||
<p class="repo-text-secondary repo-text-sm">A: ap_ds 内置了多层下载策略,会自动处理 SSL 问题。如仍遇到问题,请手动下载 SDL2 库。</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="repo-font-medium">Q: 如何获取 DAP 播放记录?</div>
|
||||
<p class="repo-text-secondary repo-text-sm">A: 使用 get_dap_recordings() 方法获取,或使用 save_dap_to_json() 保存为文件。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,679 @@
|
||||
<!-- 英文 SEO 标签 -->
|
||||
<meta name="description" content="ap_ds is an ultra-lightweight Python audio library (2.5MB) for high-quality playback & precise metadata parsing of MP3, FLAC, OGG, WAV. Zero external dependencies, non-blocking architecture. Created by Dvs (DvsXT), released 2026-03-01. A precise revolution in Python audio.">
|
||||
<meta name="keywords" content="Python audio library, lightweight audio player, MP3, FLAC, OGG, WAV metadata, non-blocking playback, ap_ds, DvsXT, zero dependencies, 2026 Python ecosystem">
|
||||
<meta property="og:title" content="ap_ds: Ultra-Lightweight Python Audio Library (2.5MB)">
|
||||
<meta property="og:description" content="Zero external deps, non-blocking playback, precise metadata parsing for MP3/FLAC/OGG/WAV. Released 2026-03-01 by Dvs (DvsXT).">
|
||||
<meta property="og:type" content="software">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="ap_ds - Python Audio Library, 2.5MB">
|
||||
<meta name="twitter:description" content="Solve the lightweight vs completeness contradiction in Python audio. Non-blocking, zero deps, 4 major formats.">
|
||||
|
||||
<!-- 中文 SEO 标签 -->
|
||||
<meta name="description" lang="zh-CN" content="ap_ds 是一个超轻量级 Python 音频库(仅 2.5MB),支持 MP3、FLAC、OGG、WAV 四种主流格式的高质量播放和精确元数据解析。零外部 Python 依赖,非阻塞播放架构让 GUI 应用流畅运行。由 Dvs (DvsXT) 创作,发布于 2026-03-01,是 Python 音频领域的精准革命。">
|
||||
<meta name="keywords" lang="zh-CN" content="Python音频库, 轻量级音频播放, MP3, FLAC, OGG, WAV元数据解析, 非阻塞播放, ap_ds, DvsXT, 零依赖, 2026 Python生态">
|
||||
<meta property="og:title" content="ap_ds:超轻量级 Python 音频库 (2.5MB)">
|
||||
<meta property="og:description" lang="zh-CN" content="零外部依赖,非阻塞播放,精准解析 MP3/FLAC/OGG/WAV。2026-03-01 由 Dvs (DvsXT) 发布。终结 Python 音频库的「完整性与轻量化」矛盾。">
|
||||
|
||||
<!-- 新增:官方自建代码仓库说明 -->
|
||||
<meta name="description" content="This domain is the official self-hosted code repository for ap_ds. Direct source, stable releases, and authoritative documentation.">
|
||||
<meta name="description" lang="zh-CN" content="本域名是 ap_ds 官方自建代码仓库。提供直接源代码、稳定版本及权威文档。"> {% extends "base.html" %}
|
||||
|
||||
{% block title %}ap_ds · lightweight Python audio solution ap_ds · 轻量级Python音频库{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="canonical" href="https://apds.top" />
|
||||
<link rel="me" href="https://gitee.com/dssxt/ap_ds" />
|
||||
<link rel="me" href="https://gitcode.com/dvsxt/ap_ds" />
|
||||
<link rel="nofollow" href="https://github.com/dvs-web/ap_ds" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script>
|
||||
<style>
|
||||
/* 首页自定义样式 - 基于 css.css 变量 */
|
||||
.home-container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: var(--repo-spacing-6) var(--repo-spacing-4);
|
||||
}
|
||||
|
||||
/* Hero 区域 */
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
padding: var(--repo-spacing-10) 0 var(--repo-spacing-8);
|
||||
background: linear-gradient(135deg, var(--repo-color-bg) 0%, var(--repo-color-bg-secondary) 100%);
|
||||
border-radius: var(--repo-radius-xl);
|
||||
margin-bottom: var(--repo-spacing-8);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--repo-spacing-4);
|
||||
background: linear-gradient(135deg, var(--repo-color-primary) 0%, #6366f1 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--repo-font-size-xl);
|
||||
color: var(--repo-color-text-secondary);
|
||||
margin-bottom: var(--repo-spacing-6);
|
||||
}
|
||||
|
||||
.version-badges {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--repo-spacing-3);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--repo-spacing-4);
|
||||
}
|
||||
|
||||
/* 目录 */
|
||||
.toc-section {
|
||||
background: var(--repo-color-bg-secondary);
|
||||
border-radius: var(--repo-radius-lg);
|
||||
padding: var(--repo-spacing-6);
|
||||
margin-bottom: var(--repo-spacing-8);
|
||||
border-left: 4px solid var(--repo-color-primary);
|
||||
}
|
||||
|
||||
.toc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--repo-spacing-2);
|
||||
margin-top: var(--repo-spacing-4);
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
color: var(--repo-color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--repo-font-size-sm);
|
||||
padding: var(--repo-spacing-1) 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toc-link:hover {
|
||||
color: var(--repo-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 内容区块 */
|
||||
.content-card {
|
||||
background: var(--repo-color-bg);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg);
|
||||
padding: var(--repo-spacing-8);
|
||||
margin-bottom: var(--repo-spacing-8);
|
||||
box-shadow: var(--repo-shadow-sm);
|
||||
}
|
||||
|
||||
.content-card h2 {
|
||||
font-size: var(--repo-font-size-2xl);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--repo-spacing-6);
|
||||
padding-bottom: var(--repo-spacing-3);
|
||||
border-bottom: 2px solid var(--repo-color-border);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.content-card h3 {
|
||||
font-size: var(--repo-font-size-xl);
|
||||
font-weight: 600;
|
||||
margin-top: var(--repo-spacing-6);
|
||||
margin-bottom: var(--repo-spacing-4);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.content-card h4 {
|
||||
font-size: var(--repo-font-size-lg);
|
||||
font-weight: 600;
|
||||
margin-top: var(--repo-spacing-4);
|
||||
margin-bottom: var(--repo-spacing-3);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
/* 特性网格 */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: var(--repo-spacing-5);
|
||||
margin-bottom: var(--repo-spacing-8);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--repo-color-bg-secondary);
|
||||
border-radius: var(--repo-radius-lg);
|
||||
padding: var(--repo-spacing-5);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--repo-shadow-md);
|
||||
border-color: var(--repo-color-primary);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: var(--repo-spacing-3);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
font-size: var(--repo-font-size-lg);
|
||||
margin-bottom: var(--repo-spacing-2);
|
||||
color: var(--repo-color-text);
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
color: var(--repo-color-text-secondary);
|
||||
font-size: var(--repo-font-size-sm);
|
||||
}
|
||||
|
||||
/* 对比表格 */
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
padding: var(--repo-spacing-3) var(--repo-spacing-4);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: var(--repo-color-bg-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 提示框 */
|
||||
.note-box {
|
||||
background: #eef2ff;
|
||||
border-left: 4px solid var(--repo-color-primary);
|
||||
border-radius: var(--repo-radius-md);
|
||||
padding: var(--repo-spacing-4) var(--repo-spacing-6);
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fffbeb;
|
||||
border-left: 4px solid #f59e0b;
|
||||
border-radius: var(--repo-radius-md);
|
||||
padding: var(--repo-spacing-4) var(--repo-spacing-6);
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background: #ecfdf5;
|
||||
border-left: 4px solid #10b981;
|
||||
border-radius: var(--repo-radius-md);
|
||||
padding: var(--repo-spacing-4) var(--repo-spacing-6);
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.code-block {
|
||||
position: relative;
|
||||
background: #0f172a;
|
||||
border-radius: var(--repo-radius-lg);
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
padding: var(--repo-spacing-5);
|
||||
overflow-x: auto;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: var(--repo-font-family-mono);
|
||||
font-size: var(--repo-font-size-sm);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
background: #334155;
|
||||
color: #f1f5f9;
|
||||
border: none;
|
||||
border-radius: var(--repo-radius-full);
|
||||
padding: 4px 14px;
|
||||
font-size: var(--repo-font-size-xs);
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
opacity: 1;
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.copy-btn.copied {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
/* 许可证区域 */
|
||||
.license-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--repo-spacing-4);
|
||||
margin-top: var(--repo-spacing-6);
|
||||
}
|
||||
|
||||
.license-card {
|
||||
background: var(--repo-color-bg-secondary);
|
||||
border-radius: var(--repo-radius-lg);
|
||||
padding: var(--repo-spacing-5);
|
||||
border: 1px solid var(--repo-color-border);
|
||||
}
|
||||
|
||||
.license-card h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--repo-spacing-3);
|
||||
}
|
||||
|
||||
/* 公告区域 */
|
||||
.announcement-content {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.announcement-content h1,
|
||||
.announcement-content h2,
|
||||
.announcement-content h3 {
|
||||
margin-top: var(--repo-spacing-5);
|
||||
margin-bottom: var(--repo-spacing-3);
|
||||
}
|
||||
|
||||
.announcement-content h1 { font-size: var(--repo-font-size-xl); border-bottom: 1px solid var(--repo-color-border); padding-bottom: var(--repo-spacing-2); }
|
||||
.announcement-content h2 { font-size: var(--repo-font-size-lg); border-bottom: 1px solid var(--repo-color-border); padding-bottom: var(--repo-spacing-2); }
|
||||
.announcement-content h3 { font-size: var(--repo-font-size-base); }
|
||||
.announcement-content p { margin-bottom: var(--repo-spacing-4); }
|
||||
.announcement-content ul, .announcement-content ol { padding-left: var(--repo-spacing-6); margin-bottom: var(--repo-spacing-4); }
|
||||
.announcement-content code {
|
||||
background: var(--repo-color-bg-tertiary);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: var(--repo-radius-sm);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.announcement-content pre {
|
||||
background: #0f172a;
|
||||
padding: var(--repo-spacing-4);
|
||||
border-radius: var(--repo-radius-md);
|
||||
overflow-x: auto;
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
}
|
||||
.announcement-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.announcement-content blockquote {
|
||||
border-left: 4px solid var(--repo-color-border);
|
||||
padding-left: var(--repo-spacing-4);
|
||||
margin: var(--repo-spacing-4) 0;
|
||||
color: var(--repo-color-text-secondary);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.content-card {
|
||||
padding: var(--repo-spacing-5);
|
||||
}
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="home-container">
|
||||
<!-- Hero 区域 -->
|
||||
<div class="hero-section">
|
||||
<h1 class="hero-title" id="hero-title">ap_ds: 轻量级 Python 音频革命</h1>
|
||||
<p class="hero-subtitle" id="hero-subtitle">2.5MB 终结 160MB 臃肿时代</p>
|
||||
<div class="version-badges" id="version-badges">
|
||||
<span class="repo-tag repo-tag--primary">v3.0.0 LTS</span>
|
||||
<span class="repo-tag repo-tag--success">仅 2.5MB</span>
|
||||
<span class="repo-tag repo-tag--warning">跨平台 W/M/L</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 目录 -->
|
||||
<div class="toc-section">
|
||||
<h3 class="repo-text-lg repo-font-semibold repo-mb-4"><i class="fa fa-list-ul" style="margin-right: 8px; color: var(--repo-color-primary);"></i>目录</h3>
|
||||
<div class="toc-grid">
|
||||
<a href="#introduction" class="toc-link">1. 项目介绍</a>
|
||||
<a href="#features" class="toc-link">2. 核心特性</a>
|
||||
<a href="#comparison" class="toc-link">3. 技术对比</a>
|
||||
<a href="#installation" class="toc-link">4. 安装与使用</a>
|
||||
<a href="#license" class="toc-link">5. 开源许可证</a>
|
||||
<a href="#faq" class="toc-link">6. 常见问题</a>
|
||||
<a href="#contact" class="toc-link">7. 联系与支持</a>
|
||||
<a href="#announcement" class="toc-link">8. 最新公告</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 项目介绍 -->
|
||||
<div class="content-card" id="introduction">
|
||||
<h2>1. 项目介绍</h2>
|
||||
<div class="note-box">
|
||||
<p><strong>📋 官方信息</strong></p>
|
||||
<p>项目名称:ap_ds</p>
|
||||
<p>官方网站:<a href="https://apds.top" style="color: var(--repo-color-primary);">https://apds.top</a></p>
|
||||
<p>PyPI:<a href="https://pypi.org/project/ap_ds/" style="color: var(--repo-color-primary);">https://pypi.org/project/ap_ds/</a></p>
|
||||
<p>开发者:Dvs (DvsXT)</p>
|
||||
</div>
|
||||
<p>ap_ds 是一个极轻量级(2.5MB)的 Python 音频库,用于播放和精确解析 MP3、FLAC、OGG 和 WAV 文件。它没有外部 Python 依赖,仅使用 Python 标准库,为非阻塞播放提供平滑的 GUI 应用程序体验。</p>
|
||||
<div class="success-box">
|
||||
<p><strong>🎉 v3.0.0 LTS 长期支持版本</strong></p>
|
||||
<p>首个长期支持版本,提供 <strong>永久免费技术支持</strong>,哈希验证下载,确定性资源清理。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 核心特性 -->
|
||||
<div class="content-card" id="features">
|
||||
<h2>2. 核心特性</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<div class="feature-title">极致轻量</div>
|
||||
<div class="feature-desc">Windows 2.5MB · macOS 3.36MB · Linux 就绪</div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎵</div>
|
||||
<div class="feature-title">四种格式</div>
|
||||
<div class="feature-desc">MP3 / FLAC / OGG / WAV</div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<div class="feature-title">精确元数据</div>
|
||||
<div class="feature-desc">WAV/FLAC 100% 准确</div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">▶️</div>
|
||||
<div class="feature-title">非阻塞播放</div>
|
||||
<div class="feature-desc">完美适配 GUI 应用</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-8);">
|
||||
<div>
|
||||
<h3>📦 体积对比</h3>
|
||||
<ul style="list-style: none; padding-left: 0;">
|
||||
<li style="margin-bottom: var(--repo-spacing-2);">🟢 <strong>ap_ds</strong> Windows: 2.5MB</li>
|
||||
<li style="margin-bottom: var(--repo-spacing-2);">🍎 macOS: 3.36MB</li>
|
||||
<li style="margin-bottom: var(--repo-spacing-2);">🐧 Linux: 动态链接 ~50KB</li>
|
||||
<li style="margin-bottom: var(--repo-spacing-2);">🔴 FFmpeg: ≥160MB</li>
|
||||
<li>🟡 Pygame + 解析库: 臃肿且功能不全</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>⚙️ 为什么这么小?</h3>
|
||||
<ul>
|
||||
<li>专注:仅播放和解析,无编辑/转码</li>
|
||||
<li>基于工业级 SDL2</li>
|
||||
<li>逐字节优化</li>
|
||||
<li>跨平台 W/M/L</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 技术对比 -->
|
||||
<div class="content-card" id="comparison">
|
||||
<h2>3. 技术对比</h2>
|
||||
|
||||
<h3>第一章:Pygame —— 游戏引擎的"音频残疾"</h3>
|
||||
<div class="code-block">
|
||||
<pre class="language-python"><code># Pygame 不可靠的音频时长
|
||||
my_sound = pygame.mixer.Sound('my_song.mp3')
|
||||
total_length = my_sound.get_length() # 仅对 WAV 可靠!</code></pre>
|
||||
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
|
||||
</div>
|
||||
<div class="warning-box">
|
||||
⚠️ Pygame 的 <code>Sound.get_length()</code> 仅对 WAV 格式正确,MP3/OGG/FLAC 返回无效值。
|
||||
</div>
|
||||
|
||||
<h3>第二章:FFmpeg 套件 —— 160MB 的"至少"之痛</h3>
|
||||
<table class="comparison-table">
|
||||
<thead><tr><th>组件</th><th>功能</th><th>大小</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>ffplay.exe</td><td>播放核心</td><td>≈80MB</td></tr>
|
||||
<tr><td>ffprobe.exe</td><td>元数据探测</td><td>≈80MB</td></tr>
|
||||
<tr><td>ffmpeg.exe</td><td>转换器</td><td>≈80MB</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="note-box">🔍 三个独立可执行文件,无 Python 封装,总计 ≥160MB。</div>
|
||||
|
||||
<h3>第三章:ap_ds 革命</h3>
|
||||
<div class="success-box">
|
||||
<p><strong>七大技术突破</strong></p>
|
||||
<ul>
|
||||
<li>✓ 2.5MB 解决 160MB 问题</li>
|
||||
<li>✓ 非阻塞消除 GUI 卡顿</li>
|
||||
<li>✓ 精确进度查询</li>
|
||||
<li>✓ 零依赖部署</li>
|
||||
<li>✓ AID 多音频管理</li>
|
||||
<li>✓ SDL2 性能</li>
|
||||
<li>✓ 智能 C 依赖跨平台</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 安装与使用 -->
|
||||
<div class="content-card" id="installation">
|
||||
<h2>4. 安装与使用</h2>
|
||||
|
||||
<div class="code-block">
|
||||
<pre class="language-bash"><code>pip install ap_ds</code></pre>
|
||||
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
|
||||
</div>
|
||||
|
||||
<h3>基础示例</h3>
|
||||
<div class="code-block">
|
||||
<pre class="language-python"><code>from ap_ds import AudioLibrary
|
||||
|
||||
lib = AudioLibrary()
|
||||
aid = lib.play_from_file("music.mp3")
|
||||
lib.pause_audio(aid)
|
||||
lib.seek_audio(aid, 30.5)
|
||||
duration = lib.stop_audio(aid)</code></pre>
|
||||
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
|
||||
</div>
|
||||
|
||||
<h3>DAP 播放列表 (v2.3.0+)</h3>
|
||||
<div class="code-block">
|
||||
<pre class="language-python"><code>aid1 = lib.play_from_file("song1.mp3")
|
||||
recordings = lib.get_dap_recordings()
|
||||
lib.save_dap_to_json("my_playlist.ap-ds-dap")</code></pre>
|
||||
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
|
||||
</div>
|
||||
|
||||
<h3>淡入淡出效果 (v2.4.0+)</h3>
|
||||
<div class="code-block">
|
||||
<pre class="language-python"><code># 2秒淡入
|
||||
lib.fadein_music(aid, loops=-1, ms=2000)
|
||||
|
||||
# 3秒淡出
|
||||
lib.fadeout_music(ms=3000)
|
||||
|
||||
# 检查播放状态
|
||||
if lib.is_music_playing():
|
||||
print("音乐播放中")</code></pre>
|
||||
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 开源许可证 -->
|
||||
<div class="content-card" id="license">
|
||||
<h2>5. 开源许可证</h2>
|
||||
<div class="license-grid">
|
||||
<div class="license-card">
|
||||
<h4>✅ 您可以</h4>
|
||||
<ul>
|
||||
<li>商业使用</li>
|
||||
<li>修改代码</li>
|
||||
<li>闭源集成</li>
|
||||
<li>销售解决方案</li>
|
||||
<li>云服务部署</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="license-card">
|
||||
<h4>❌ 您不可以</h4>
|
||||
<ul>
|
||||
<li>使用原始品牌名称</li>
|
||||
<li>删除版权声明</li>
|
||||
<li>用于非法用途</li>
|
||||
<li>专利侵权诉讼</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="license-card">
|
||||
<h4>👍 您应该</h4>
|
||||
<ul>
|
||||
<li>保留版权声明</li>
|
||||
<li>标注来源归属</li>
|
||||
<li>报告安全问题</li>
|
||||
<li>修改版本用独立品牌</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="repo-text-secondary repo-text-sm repo-mt-4">详细许可证请查看 <code>AP_DS_LICENSE.MD</code> 文件。</p>
|
||||
</div>
|
||||
|
||||
<!-- 6. 常见问题 -->
|
||||
<div class="content-card" id="faq">
|
||||
<h2>6. 常见问题</h2>
|
||||
|
||||
<div><p class="repo-font-semibold">问:公司可以将其集成到商业产品中吗?</p><div class="success-box">✅ 完全可以,无需付费、无需通知、无需授权。</div></div>
|
||||
|
||||
<div><p class="repo-font-semibold repo-mt-4">问:我可以修改并重新分发吗?</p><div class="success-box">✅ 可以,但必须使用独立品牌、保留版权、标注来源、提供维护者声明。</div></div>
|
||||
|
||||
<div><p class="repo-font-semibold repo-mt-4">问:我必须开源我的修改吗?</p><div class="warning-box">❌ 不需要,您可以闭源分发。</div></div>
|
||||
|
||||
<div><p class="repo-font-semibold repo-mt-4">问:在云服务中使用需要付费吗?</p><div class="warning-box">❌ 不需要,但欢迎贡献代码。</div></div>
|
||||
|
||||
<div><p class="repo-font-semibold repo-mt-4">问:技术支持收费吗?</p><div class="success-box">✅ 永久免费技术支持,通过 GitCode Issues 或邮件联系。</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 7. 联系与支持 -->
|
||||
<div class="content-card" id="contact">
|
||||
<h2>7. 联系与支持</h2>
|
||||
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-6);">
|
||||
<div style="background: var(--repo-color-bg-secondary); padding: var(--repo-spacing-5); border-radius: var(--repo-radius-lg);">
|
||||
<h4>📧 授权咨询</h4>
|
||||
<p>me@dvsyun.top 或 dvs6666@163.com</p>
|
||||
<p class="repo-text-sm repo-text-tertiary">7个工作日内回复</p>
|
||||
</div>
|
||||
<div style="background: var(--repo-color-bg-secondary); padding: var(--repo-spacing-5); border-radius: var(--repo-radius-lg);">
|
||||
<h4>🛠️ 技术支持</h4>
|
||||
<p>GitCode Issues · 官方文档 · 邮件支持</p>
|
||||
<p class="repo-text-sm repo-text-success">✅ 永久免费</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-mt-6" style="background: var(--repo-color-bg-secondary); border-radius: var(--repo-radius-lg); padding: var(--repo-spacing-5);">
|
||||
<h3>🔗 官方仓库</h3>
|
||||
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-4); margin-top: var(--repo-spacing-3);">
|
||||
<div>
|
||||
<p>✅ <strong>主仓库:</strong><a href="https://gitcode.com/dvsxt/ap_ds" class="repo-text-link">GitCode</a></p>
|
||||
<p>✅ <strong>国内镜像:</strong><a href="https://gitee.com/dssxt/ap_ds" class="repo-text-link">Gitee</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>❌ <strong>已废弃:</strong><a href="https://github.com/dvs-web/ap_ds" class="repo-text-danger">GitHub</a></p>
|
||||
<p>🔗 <strong>项目主页:</strong><a href="https://apds.top" class="repo-text-link">apds.top</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 8. 最新公告 -->
|
||||
<div class="content-card" id="announcement">
|
||||
<h2>8. 最新公告</h2>
|
||||
<div id="announcement-content" class="announcement-content">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 复制代码函数
|
||||
window.copyCode = function(btn) {
|
||||
const pre = btn.parentElement.querySelector('pre');
|
||||
const code = pre.innerText;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
btn.classList.add('copied');
|
||||
btn.innerHTML = '<i class="fa fa-check"></i> 已复制';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.innerHTML = '<i class="fa fa-clipboard"></i> 复制';
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// 从后端获取版本信息
|
||||
fetch('/api/version-info')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.title) document.getElementById('hero-title').innerHTML = data.title;
|
||||
if (data.subtitle) document.getElementById('hero-subtitle').innerHTML = data.subtitle;
|
||||
const badgesDiv = document.getElementById('version-badges');
|
||||
if (data.version || data.size) {
|
||||
badgesDiv.innerHTML = `
|
||||
<span class="repo-tag repo-tag--primary">${data.version || 'v3.0.0 LTS'}</span>
|
||||
<span class="repo-tag repo-tag--success">仅 ${data.size || '2.5MB'}</span>
|
||||
<span class="repo-tag repo-tag--warning">跨平台 W/M/L</span>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 加载最新公告
|
||||
fetch('/api/latest-announcement')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('announcement-content');
|
||||
if (data.html) {
|
||||
container.innerHTML = data.html;
|
||||
// 高亮代码块
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAllUnder(container);
|
||||
}
|
||||
// 为代码块添加复制按钮
|
||||
container.querySelectorAll('pre').forEach(pre => {
|
||||
if (!pre.parentElement.classList.contains('code-block')) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block';
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.innerHTML = '<i class="fa fa-clipboard"></i> 复制';
|
||||
btn.onclick = function() { window.copyCode(btn); };
|
||||
wrapper.appendChild(btn);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = '<p class="repo-text-tertiary">暂无公告</p>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('announcement-content').innerHTML = '<p class="repo-text-tertiary">无法加载公告</p>';
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,297 @@
|
||||
|
||||
<!-- 英文 SEO 标签 -->
|
||||
<meta name="description" content="Official releases page for ap_ds. Full version list: v3.0.0 LTS (current stable), branch APDSLTS001, release date 2026-03-01. Each version includes ZIP/TAR.GZ/WHL downloads, detailed changelogs (deterministic cleanup, hash verification), upgrade guides, and version status labels (stable/withdrawn/incident).">
|
||||
<meta name="keywords" content="ap_ds releases, download v3.0.0, changelog, LTS version, Python audio library stable release, APDSLTS001, hash verification, deterministic cleanup, WHL download">
|
||||
<meta property="og:title" content="ap_ds Releases – v3.0.0 LTS & Version Archive">
|
||||
<meta property="og:description" content="Official ap_ds release history. Download stable builds, read detailed changelogs, check version status (stable/withdrawn/incident).">
|
||||
<meta property="og:type" content="software">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="ap_ds Releases & Changelogs">
|
||||
|
||||
<!-- 中文 SEO 标签 -->
|
||||
<meta name="description" lang="zh-CN" content="ap_ds 官方发行版页面。完整版本列表:v3.0.0 LTS(当前稳定版),分支 APDSLTS001,发布日期 2026-03-01。每个版本附 ZIP/TAR.GZ/WHL 下载、详细更新日志(确定性资源清理、哈希验证)、升级指南及版本状态标注(稳定版/已撤回/开发事故)。">
|
||||
<meta name="keywords" lang="zh-CN" content="ap_ds 发行版, 下载 v3.0.0, 更新日志, LTS版本, Python音频库稳定版, APDSLTS001, 哈希验证, 确定性清理, WHL下载">
|
||||
<meta property="og:title" lang="zh-CN" content="ap_ds 发行版 – v3.0.0 LTS 及版本归档">
|
||||
<meta property="og:description" lang="zh-CN" content="ap_ds 官方版本历史。下载稳定版,阅读详细更新日志,查看版本状态(稳定/已撤回/开发事故)。">{% extends "base.html" %}{% block content %}{% block title %}ap_ds Releases – Version History, Changelogs & Stable Downloads (v3.0.0 LTS){% endblock %}
|
||||
<div class="repo-header">
|
||||
<div class="repo-path">
|
||||
<span>📦 发行版</span>
|
||||
</div>
|
||||
<p style="color: var(--repo-color-text-tertiary); margin-top: 8px;">所有 ap_ds 版本下载</p>
|
||||
</div>
|
||||
|
||||
<!-- 版本列表 -->
|
||||
<div class="file-list">
|
||||
<div class="file-list-header">
|
||||
<span>版本</span>
|
||||
<span>分支</span>
|
||||
<span>发布日期</span>
|
||||
<span>下载</span>
|
||||
</div>
|
||||
{% for release in releases %}
|
||||
<div class="file-row">
|
||||
<div class="file-name" style="flex: 0.3;">
|
||||
<span class="icon">🏷️</span>
|
||||
<strong style="color: var(--repo-color-primary);">{{ release.version }}</strong>
|
||||
</div>
|
||||
<div class="file-message" style="flex: 0.3;">{{ release.branch }}</div>
|
||||
<div class="file-time" style="flex: 0.2;">{{ release.date }}</div>
|
||||
<div style="display: flex; gap: 8px; flex: 0.2;">
|
||||
<a href="{{ url_for('download_zip', branch=release.branch, version=release.version) }}" class="btn btn-secondary" style="padding: 4px 8px;">ZIP</a>
|
||||
<a href="{{ url_for('download_tar', branch=release.branch, version=release.version) }}" class="btn btn-secondary" style="padding: 4px 8px;">TAR.GZ</a>
|
||||
<a href="{{ url_for('download_whl', version=release.version) }}" class="btn btn-primary" style="padding: 4px 8px;">WHL</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 版本详情 - 展开/收起 -->
|
||||
<div class="releases-details">
|
||||
{% for release in releases %}
|
||||
<div class="release-card" data-version="{{ release.version }}">
|
||||
<div class="release-header" onclick="toggleRelease('{{ release.version }}')">
|
||||
<div class="release-header-left">
|
||||
<span class="release-version">{{ release.version }}</span>
|
||||
<span class="release-branch">{{ release.branch }}</span>
|
||||
<span class="release-date">{{ release.date }}</span>
|
||||
</div>
|
||||
<div class="release-header-right">
|
||||
<span class="release-status">
|
||||
{% if release.has_whl %}
|
||||
<span class="repo-tag repo-tag--success">PyPI</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="release-toggle" id="toggle-{{ release.version }}">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="release-body" id="release-{{ release.version }}" style="display: none;">
|
||||
<div class="release-content">
|
||||
{% if release.announcement %}
|
||||
{{ release.announcement|safe }}
|
||||
{% else %}
|
||||
<div class="info-message">
|
||||
📄 暂无版本说明文档。请查看 <a href="{{ url_for('view_file', file_path=release.branch + '/' + release.version + '/README.md', branch=release.branch, version=release.version) }}">README.md</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="release-downloads">
|
||||
<a href="{{ url_for('download_zip', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📦 下载 ZIP</a>
|
||||
<a href="{{ url_for('download_tar', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📦 下载 TAR.GZ</a>
|
||||
<a href="{{ url_for('download_whl', version=release.version) }}" class="btn btn-primary">📦 下载 WHL</a>
|
||||
<a href="{{ url_for('view_file', file_path=release.branch + '/' + release.version + '/README.md', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📖 查看 README</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.releases-details {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.release-card {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
border-radius: var(--repo-radius-lg, 8px);
|
||||
overflow: hidden;
|
||||
background-color: var(--repo-color-bg);
|
||||
}
|
||||
|
||||
.release-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.release-header:hover {
|
||||
background-color: var(--repo-color-bg-hover);
|
||||
}
|
||||
|
||||
.release-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.release-version {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--repo-color-primary);
|
||||
}
|
||||
|
||||
.release-branch {
|
||||
font-size: 13px;
|
||||
color: var(--repo-color-text-secondary);
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.release-date {
|
||||
font-size: 13px;
|
||||
color: var(--repo-color-text-tertiary);
|
||||
}
|
||||
|
||||
.release-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.release-toggle {
|
||||
font-size: 16px;
|
||||
color: var(--repo-color-text-tertiary);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.release-card.open .release-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.release-body {
|
||||
padding: 24px;
|
||||
border-top: 1px solid var(--repo-color-border);
|
||||
background-color: var(--repo-color-bg);
|
||||
}
|
||||
|
||||
.release-content {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--repo-color-border);
|
||||
}
|
||||
|
||||
/* README 内容样式 */
|
||||
.release-content h1, .release-content h2, .release-content h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.release-content h1 { font-size: 1.8em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
.release-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
|
||||
.release-content h3 { font-size: 1.25em; }
|
||||
.release-content h4 { font-size: 1.1em; }
|
||||
.release-content p { margin-bottom: 16px; line-height: 1.6; }
|
||||
.release-content code {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-family: var(--repo-font-family-mono, monospace);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.release-content pre {
|
||||
background-color: var(--repo-color-bg-tertiary);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.release-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.release-content ul, .release-content ol {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.release-content blockquote {
|
||||
border-left: 4px solid var(--repo-color-border);
|
||||
padding-left: 16px;
|
||||
color: var(--repo-color-text-tertiary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.release-content table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.release-content th, .release-content td {
|
||||
border: 1px solid var(--repo-color-border);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.release-content th {
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
}
|
||||
.release-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--repo-color-border);
|
||||
margin: 24px 0;
|
||||
}
|
||||
.release-content img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.release-downloads {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 16px;
|
||||
background-color: var(--repo-color-bg-secondary);
|
||||
border-radius: var(--repo-radius-md, 6px);
|
||||
color: var(--repo-color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.repo-tag--success {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.release-header-left {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.release-downloads {
|
||||
flex-direction: column;
|
||||
}
|
||||
.release-downloads .btn {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleRelease(version) {
|
||||
var body = document.getElementById('release-' + version);
|
||||
var card = document.querySelector('.release-card[data-version="' + version + '"]');
|
||||
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
card.classList.add('open');
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
card.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
// 默认展开第一个版本
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var firstRelease = document.querySelector('.release-card');
|
||||
if (firstRelease) {
|
||||
var version = firstRelease.getAttribute('data-version');
|
||||
if (version) {
|
||||
var body = document.getElementById('release-' + version);
|
||||
if (body) {
|
||||
body.style.display = 'block';
|
||||
firstRelease.classList.add('open');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ metadata.name }} · ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-header">
|
||||
<div class="repo-path">
|
||||
<a href="/code">ap_ds</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}">{{ branch }}</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}">{{ version }}</a>
|
||||
{% if branch and version %}
|
||||
{# 将反斜杠替换为正斜杠 #}
|
||||
{% set normalized_path = metadata.path.replace('\\', '/') %}
|
||||
{% set relative_path = normalized_path.replace(branch + '/' + version + '/', '') %}
|
||||
{% if relative_path and '/' in relative_path %}
|
||||
{% set parts = relative_path.split('/') %}
|
||||
{% for part in parts[:-1] %}
|
||||
<span class="separator">/</span>
|
||||
<a href="#">{{ part }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<span class="separator">/</span>
|
||||
<span>{{ metadata.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-header">
|
||||
<div>
|
||||
<span style="font-family: monospace;">{{ metadata.name }}</span>
|
||||
<span style="margin-left: 12px; font-size: 12px; color: #848d97;">{{ metadata.size }} 字节</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="copy-btn" onclick="copyCode(this)">复制代码</button>
|
||||
<a href="{{ url_for('download_file', file_path=metadata.path.replace('\\', '/')) }}" class="copy-btn" style="text-decoration: none;">下载文件</a>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="copy-btn" style="text-decoration: none;">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-content">
|
||||
<pre><code class="language-python" id="code-content">{{ content|escape }}</code></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
hljs.highlightElement(document.querySelector('#code-content'));
|
||||
});
|
||||
|
||||
function copyCode(btn) {
|
||||
var code = document.getElementById('code-content').innerText;
|
||||
navigator.clipboard.writeText(code).then(function() {
|
||||
var originalText = btn.innerText;
|
||||
btn.innerText = '已复制!';
|
||||
setTimeout(function() { btn.innerText = originalText; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ metadata.name }} - ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-container">
|
||||
<div class="repo-card repo-mb-4">
|
||||
<div class="repo-card__body">
|
||||
<div class="repo-flex repo-justify-between repo-items-center">
|
||||
<div>
|
||||
<h2 class="repo-text-lg repo-font-semibold">{{ metadata.name }}</h2>
|
||||
<div class="repo-text-sm repo-text-tertiary repo-mt-1">
|
||||
{{ metadata.size }} 字节 · 最后修改 {{ metadata.modified }} · 类型: {{ metadata.extension }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-flex repo-gap-2">
|
||||
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="repo-btn repo-btn--sm repo-btn--primary">下载文件</a>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-btn repo-btn--sm repo-btn--ghost">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_text %}
|
||||
<div class="repo-card">
|
||||
<div class="repo-card__body">
|
||||
<pre style="background: var(--repo-color-bg-tertiary); padding: 1rem; border-radius: 8px; overflow-x: auto; font-family: monospace; font-size: 13px;">{{ content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="repo-card repo-text-center repo-py-8">
|
||||
<div class="repo-card__body">
|
||||
<span class="icon-file" style="font-size: 4rem; color: var(--repo-color-text-tertiary);"></span>
|
||||
<p class="repo-text-secondary repo-mt-4">此文件类型无法在线预览</p>
|
||||
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="repo-btn repo-btn--primary repo-mt-4">下载文件</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ metadata.name }} · ap_ds{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="repo-header">
|
||||
<div class="repo-path">
|
||||
<a href="/code">ap_ds</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}">{{ branch }}</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}">{{ version }}</a>
|
||||
<span class="separator">/</span>
|
||||
<span>{{ metadata.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-header">
|
||||
<div>
|
||||
<span style="font-family: monospace;">{{ metadata.name }}</span>
|
||||
<span style="margin-left: 12px; font-size: 12px; color: var(--repo-color-text-tertiary);">{{ metadata.size }} 字节</span>
|
||||
<span style="margin-left: 12px; font-size: 12px; color: var(--repo-color-text-tertiary);">最后修改 {{ metadata.modified }}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="copy-btn" style="text-decoration: none;">下载文件</a>
|
||||
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="copy-btn" style="text-decoration: none;">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关键修改:使用 |safe 过滤器 -->
|
||||
<div class="readme-content" style="border: 1px solid var(--repo-color-border); border-top: none; border-radius: 0 0 var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px);">
|
||||
{{ html_content|safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user