feat: 增加桌面端登录、视频清单和封面缓存
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target-dir = "../dist/target"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 567 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 777 B |
|
After Width: | Height: | Size: 1020 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 848 B |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||