feat: 增加桌面端登录、视频清单和封面缓存

This commit is contained in:
Nixevol
2026-09-27 03:57:00 +08:00
parent 0c1e04464a
commit cc930cd1ed
69 changed files with 9895 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
target-dir = "../dist/target"
+7
View File
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
+5015
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "raptdrama"
version = "0.1.0"
description = "RaptDrama desktop shell"
authors = ["nixevol"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "raptdrama_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Read the optimization guideline for more details: https://tauri.app/concept/size/#cargo-configuration
[profile.release]
codegen-units = 1
lto = true
opt-level = 3
panic = "abort"
strip = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>RaptDrama</title>
<style>
html, body { height: 100%; margin: 0; background: #101014; color: #f2f2f2; font: 16px/1.5 "Segoe UI", sans-serif; }
body { display: flex; align-items: center; justify-content: center; }
main { text-align: center; }
h1 { margin: 0 0 12px; font-size: 22px; font-weight: 600; }
p { margin: 0; color: #c8c8c8; }
</style>
</head>
<body>
<main>
<h1>RaptDrama</h1>
<p id="status">后台程序正在启动</p>
</main>
<script>
window.setStatus = (text) => {
document.getElementById("status").textContent = text;
};
</script>
</body>
</html>
+178
View File
@@ -0,0 +1,178 @@
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager, Url};
struct Backend {
child: Mutex<Option<Child>>,
}
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.canonicalize()
.expect("项目根目录")
}
fn python_command(root: &Path) -> PathBuf {
let bundled = root.join("server").join(".venv").join("Scripts").join("python.exe");
if bundled.is_file() {
bundled
} else {
PathBuf::from("python")
}
}
fn free_port() -> Result<u16, String> {
let listener = TcpListener::bind("127.0.0.1:0").map_err(|err| format!("没有可用端口:{err}"))?;
listener
.local_addr()
.map(|addr| addr.port())
.map_err(|err| format!("没有可用端口:{err}"))
}
fn spawn_backend(root: &Path, port: u16) -> Result<Child, String> {
let python = python_command(root);
let static_dir = root.join("dist").join("ui");
if !static_dir.join("index.html").is_file() {
return Err("页面还没有编译完成".into());
}
let mut command = Command::new(&python);
command
.current_dir(root)
.arg("-m")
.arg("uvicorn")
.arg("server.app:app")
.arg("--host")
.arg("127.0.0.1")
.arg("--port")
.arg(port.to_string())
.env("RAPTD_STATIC", &static_dir)
.env("PYTHONUNBUFFERED", "1");
#[cfg(all(windows, not(debug_assertions)))]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
command.spawn().map_err(|err| format!("无法启动后台:{err}"))
}
fn health_ok(port: u16) -> bool {
let mut stream = match TcpStream::connect(("127.0.0.1", port)) {
Ok(stream) => stream,
Err(_) => return false,
};
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
if stream
.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
.is_err()
{
return false;
}
let mut buf = Vec::new();
let mut tmp = [0_u8; 1024];
loop {
match stream.read(&mut tmp) {
Ok(0) => break,
Ok(size) => buf.extend_from_slice(&tmp[..size]),
Err(err)
if err.kind() == std::io::ErrorKind::TimedOut
|| err.kind() == std::io::ErrorKind::WouldBlock =>
{
break;
}
Err(_) => return false,
}
}
let text = String::from_utf8_lossy(&buf);
text.contains("200") && text.to_ascii_lowercase().contains("\"ok\"")
}
fn child_exited(handle: &AppHandle) -> Option<String> {
let state = handle.try_state::<Backend>()?;
let mut guard = state.child.lock().ok()?;
let child = guard.as_mut()?;
match child.try_wait() {
Ok(Some(status)) => Some(format!("后台退出了:{status}")),
_ => None,
}
}
fn stop_child(handle: &AppHandle) {
let Some(state) = handle.try_state::<Backend>() else {
return;
};
let Ok(mut guard) = state.child.lock() else {
return;
};
if let Some(mut child) = guard.take() {
let _ = child.kill();
let _ = child.wait();
}
}
fn boot(handle: &AppHandle, window: &tauri::WebviewWindow) -> Result<(), String> {
let root = repo_root();
let mut last_error = "后台启动超时".to_string();
for _ in 0..3 {
let port = free_port()?;
let child = spawn_backend(&root, port)?;
if let Some(state) = handle.try_state::<Backend>() {
let mut guard = state.child.lock().map_err(|_| "后台状态不可用".to_string())?;
*guard = Some(child);
}
let deadline = Instant::now() + Duration::from_secs(20);
while Instant::now() < deadline {
if health_ok(port) {
let url = Url::parse(&format!("http://127.0.0.1:{port}/")).map_err(|err| err.to_string())?;
eprintln!("RaptDrama http://127.0.0.1:{port}/");
let _ = std::io::Write::flush(&mut std::io::stderr());
window.navigate(url).map_err(|err| err.to_string())?;
return Ok(());
}
if let Some(message) = child_exited(handle) {
last_error = message;
break;
}
std::thread::sleep(Duration::from_millis(200));
}
stop_child(handle);
}
Err(last_error)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(|app| {
app.manage(Backend {
child: Mutex::new(None),
});
let handle = app.handle().clone();
let window = handle
.get_webview_window("main")
.expect("主窗口");
std::thread::spawn(move || {
if let Err(message) = boot(&handle, &window) {
eprintln!("后台启动失败:{message}");
let quoted = serde_json::to_string(&message).unwrap_or_else(|_| "\"启动失败\"".to_string());
let _ = window.eval(&format!("window.setStatus({quoted})"));
}
});
Ok(())
})
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app, event| {
if matches!(event, tauri::RunEvent::Exit) {
stop_child(app);
}
});
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
raptdrama_lib::run()
}
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "RaptDrama",
"version": "0.1.0",
"identifier": "com.raptdrama.desktop",
"build": {
"beforeDevCommand": {
"script": "npm run build --prefix ui",
"cwd": "..",
"wait": true
},
"beforeBuildCommand": {
"script": "npm run build --prefix ui",
"cwd": ".."
},
"frontendDist": "splash"
},
"app": {
"windows": [
{
"label": "main",
"title": "RaptDrama",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}