* feat(engine): 添加编辑器模式标志控制编辑器UI显示 - 在 Rust 引擎中添加 isEditor 标志,控制网格、gizmos、坐标轴指示器的显示 - 运行时模式下自动隐藏所有编辑器专用 UI - 编辑器预览和浏览器运行时通过 setEditorMode(false) 禁用编辑器 UI - 添加 Scene.isEditorMode 延迟组件生命周期回调,直到 begin() 调用 - 修复用户组件注册到 Core ComponentRegistry 以支持序列化 - 修复 Run in Browser 时用户组件加载问题 * fix: 复制引擎模块的类型定义文件到 dist/engine * fix: 修复用户项目 tsconfig paths 类型定义路径 - 从 module.json 读取实际包名而不是使用目录名 - 修复 .d.ts 文件复制逻辑,支持 .mjs 扩展名
712 lines
25 KiB
Rust
712 lines
25 KiB
Rust
//! System operations
|
||
//!
|
||
//! OS-level operations like opening files, showing in folder, devtools, etc.
|
||
|
||
use std::path::PathBuf;
|
||
use std::process::Command;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::thread;
|
||
use std::net::UdpSocket;
|
||
use tauri::{AppHandle, Manager};
|
||
use tiny_http::{Server, Response};
|
||
use qrcode::QrCode;
|
||
use image::Luma;
|
||
|
||
// Global server state
|
||
static SERVER_RUNNING: AtomicBool = AtomicBool::new(false);
|
||
static SERVER_STOP_FLAG: once_cell::sync::Lazy<Arc<AtomicBool>> =
|
||
once_cell::sync::Lazy::new(|| Arc::new(AtomicBool::new(false)));
|
||
|
||
/// Toggle developer tools (debug mode only)
|
||
#[tauri::command]
|
||
pub fn toggle_devtools(app: AppHandle) -> Result<(), String> {
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
if let Some(window) = app.get_webview_window("main") {
|
||
if window.is_devtools_open() {
|
||
window.close_devtools();
|
||
} else {
|
||
window.open_devtools();
|
||
}
|
||
Ok(())
|
||
} else {
|
||
Err("Window not found".to_string())
|
||
}
|
||
}
|
||
|
||
#[cfg(not(debug_assertions))]
|
||
{
|
||
let _ = app;
|
||
Err("DevTools are only available in debug mode".to_string())
|
||
}
|
||
}
|
||
|
||
/// Open file with system default application
|
||
#[tauri::command]
|
||
pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
||
#[cfg(target_os = "windows")]
|
||
{
|
||
Command::new("cmd")
|
||
.args(["/C", "start", "", &file_path])
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||
}
|
||
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
Command::new("open")
|
||
.arg(&file_path)
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
{
|
||
Command::new("xdg-open")
|
||
.arg(&file_path)
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Show file in system file explorer
|
||
#[tauri::command]
|
||
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
||
println!("[show_in_folder] Received path: {}", file_path);
|
||
|
||
#[cfg(target_os = "windows")]
|
||
{
|
||
use std::path::Path;
|
||
|
||
// Normalize path separators for Windows
|
||
// 规范化路径分隔符
|
||
let normalized_path = file_path.replace('/', "\\");
|
||
println!("[show_in_folder] Normalized path: {}", normalized_path);
|
||
|
||
// Verify the path exists before trying to show it
|
||
// 验证路径存在
|
||
let path = Path::new(&normalized_path);
|
||
let exists = path.exists();
|
||
println!("[show_in_folder] Path exists: {}", exists);
|
||
|
||
if !exists {
|
||
return Err(format!("Path does not exist: {}", normalized_path));
|
||
}
|
||
|
||
// Windows explorer requires /select, to be concatenated with the path
|
||
// without spaces. Use a single argument to avoid shell parsing issues.
|
||
// Windows 资源管理器要求 /select, 与路径连接在一起,中间没有空格
|
||
let select_arg = format!("/select,{}", normalized_path);
|
||
println!("[show_in_folder] Explorer arg: {}", select_arg);
|
||
|
||
Command::new("explorer")
|
||
.arg(&select_arg)
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||
}
|
||
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
Command::new("open")
|
||
.args(["-R", &file_path])
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
{
|
||
use std::path::Path;
|
||
let path = Path::new(&file_path);
|
||
let parent = path
|
||
.parent()
|
||
.ok_or_else(|| "Failed to get parent directory".to_string())?;
|
||
|
||
Command::new("xdg-open")
|
||
.arg(parent)
|
||
.spawn()
|
||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Get system temp directory
|
||
#[tauri::command]
|
||
pub fn get_temp_dir() -> Result<String, String> {
|
||
std::env::temp_dir()
|
||
.to_str()
|
||
.map(|s| s.to_string())
|
||
.ok_or_else(|| "Failed to get temp directory".to_string())
|
||
}
|
||
|
||
/// 使用 where 命令查找可执行文件路径
|
||
/// Use 'where' command to find executable path
|
||
#[cfg(target_os = "windows")]
|
||
fn find_command_path(cmd: &str) -> Option<String> {
|
||
use std::process::Command as StdCommand;
|
||
use std::path::Path;
|
||
|
||
// 使用 where 命令查找
|
||
let output = StdCommand::new("where")
|
||
.arg(cmd)
|
||
.output()
|
||
.ok()?;
|
||
|
||
if output.status.success() {
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
// 取第一行结果(可能有多个匹配)
|
||
if let Some(first_line) = stdout.lines().next() {
|
||
let path = first_line.trim();
|
||
if !path.is_empty() {
|
||
let path_obj = Path::new(path);
|
||
|
||
// 检查是否是 bin 目录下的脚本(VSCode/Cursor 特征)
|
||
// Check if it's a script in bin directory (VSCode/Cursor pattern)
|
||
let is_bin_script = path_obj.parent()
|
||
.map(|p| p.ends_with("bin"))
|
||
.unwrap_or(false);
|
||
|
||
// 如果找到的是 .cmd 或 .bat,或者是 bin 目录下的脚本(where 可能不返回扩展名)
|
||
// If found .cmd or .bat, or a script in bin directory (where may not return extension)
|
||
let has_script_ext = path.ends_with(".cmd") || path.ends_with(".bat");
|
||
|
||
if has_script_ext || is_bin_script {
|
||
// 尝试找 Code.exe (VSCode) 或 Cursor.exe 等
|
||
// Try to find Code.exe (VSCode) or Cursor.exe etc.
|
||
if let Some(bin_dir) = path_obj.parent() {
|
||
if let Some(parent_dir) = bin_dir.parent() {
|
||
// VSCode: bin/code.cmd -> Code.exe
|
||
let exe_path = parent_dir.join("Code.exe");
|
||
if exe_path.exists() {
|
||
let exe_str = exe_path.to_string_lossy().to_string();
|
||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||
return Some(exe_str);
|
||
}
|
||
|
||
// Cursor: bin/cursor.cmd -> Cursor.exe
|
||
let cursor_exe = parent_dir.join("Cursor.exe");
|
||
if cursor_exe.exists() {
|
||
let exe_str = cursor_exe.to_string_lossy().to_string();
|
||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||
return Some(exe_str);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
println!("[find_command_path] Found {} at: {}", cmd, path);
|
||
return Some(path.to_string());
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
#[cfg(not(target_os = "windows"))]
|
||
fn find_command_path(cmd: &str) -> Option<String> {
|
||
use std::process::Command as StdCommand;
|
||
|
||
let output = StdCommand::new("which")
|
||
.arg(cmd)
|
||
.output()
|
||
.ok()?;
|
||
|
||
if output.status.success() {
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
let path = stdout.trim();
|
||
if !path.is_empty() {
|
||
return Some(path.to_string());
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// 解析编辑器命令,返回实际可执行路径
|
||
/// Resolve editor command to actual executable path
|
||
fn resolve_editor_command(editor_command: &str) -> String {
|
||
use std::path::Path;
|
||
|
||
// 如果命令已经是完整路径且存在,直接返回
|
||
// If command is already a full path and exists, return it
|
||
if Path::new(editor_command).exists() {
|
||
return editor_command.to_string();
|
||
}
|
||
|
||
// 使用系统命令查找可执行文件路径
|
||
// Use system command to find executable path
|
||
if let Some(path) = find_command_path(editor_command) {
|
||
return path;
|
||
}
|
||
|
||
// 回退到原始命令 | Fall back to original command
|
||
editor_command.to_string()
|
||
}
|
||
|
||
/// Open project folder with specified editor
|
||
/// 使用指定编辑器打开项目文件夹
|
||
///
|
||
/// @param project_path - Project folder path | 项目文件夹路径
|
||
/// @param editor_command - Editor command (e.g., "code", "cursor") | 编辑器命令
|
||
/// @param file_path - Optional file to open (will be opened in the editor) | 可选的要打开的文件
|
||
#[tauri::command]
|
||
pub fn open_with_editor(
|
||
project_path: String,
|
||
editor_command: String,
|
||
file_path: Option<String>,
|
||
) -> Result<(), String> {
|
||
use std::path::Path;
|
||
|
||
// Normalize paths
|
||
let normalized_project = project_path.replace('/', "\\");
|
||
let normalized_file = file_path.map(|f| f.replace('/', "\\"));
|
||
|
||
// Verify project path exists
|
||
let project = Path::new(&normalized_project);
|
||
if !project.exists() {
|
||
return Err(format!("Project path does not exist: {}", normalized_project));
|
||
}
|
||
|
||
// 解析编辑器命令到实际路径
|
||
// Resolve editor command to actual path
|
||
let resolved_command = resolve_editor_command(&editor_command);
|
||
|
||
println!(
|
||
"[open_with_editor] editor: {} -> {}, project: {}, file: {:?}",
|
||
editor_command, resolved_command, normalized_project, normalized_file
|
||
);
|
||
|
||
let mut cmd = Command::new(&resolved_command);
|
||
|
||
// VSCode/Cursor CLI 正确用法:
|
||
// 1. 使用 --folder-uri 或直接传文件夹路径会打开新窗口
|
||
// 2. 使用 --add 可以将文件夹添加到当前工作区
|
||
// 3. 使用 --goto file:line:column 可以打开文件并定位
|
||
//
|
||
// VSCode/Cursor CLI correct usage:
|
||
// 1. Use --folder-uri or pass folder path directly to open new window
|
||
// 2. Use --add to add folder to current workspace
|
||
// 3. Use --goto file:line:column to open file and navigate
|
||
//
|
||
// 正确命令格式: code <folder> <file>
|
||
// 这会打开文件夹并同时打开文件
|
||
// Correct command format: code <folder> <file>
|
||
// This opens the folder and also opens the file
|
||
|
||
// Add project folder first
|
||
// 先添加项目文件夹
|
||
cmd.arg(&normalized_project);
|
||
|
||
// If a specific file is provided, add it directly (not with -g)
|
||
// VSCode will open the folder AND the file
|
||
// 如果提供了文件,直接添加(不使用 -g)
|
||
// VSCode 会同时打开文件夹和文件
|
||
if let Some(ref file) = normalized_file {
|
||
let file_path_obj = Path::new(file);
|
||
if file_path_obj.exists() {
|
||
cmd.arg(file);
|
||
}
|
||
}
|
||
|
||
cmd.spawn()
|
||
.map_err(|e| format!("Failed to open with editor '{}': {}", resolved_command, e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Get application resource directory
|
||
#[tauri::command]
|
||
pub fn get_app_resource_dir(app: AppHandle) -> Result<String, String> {
|
||
app.path()
|
||
.resource_dir()
|
||
.map_err(|e| format!("Failed to get resource directory: {}", e))
|
||
.and_then(|p| {
|
||
p.to_str()
|
||
.map(|s| s.to_string())
|
||
.ok_or_else(|| "Invalid path encoding".to_string())
|
||
})
|
||
}
|
||
|
||
/// Get current working directory
|
||
#[tauri::command]
|
||
pub fn get_current_dir() -> Result<String, String> {
|
||
std::env::current_dir()
|
||
.and_then(|p| Ok(p.to_string_lossy().to_string()))
|
||
.map_err(|e| format!("Failed to get current directory: {}", e))
|
||
}
|
||
|
||
/// Update project tsconfig.json with engine type paths
|
||
/// 更新项目的 tsconfig.json,添加引擎类型路径
|
||
///
|
||
/// Scans dist/engine/ directory and adds paths for all modules with .d.ts files.
|
||
/// 扫描 dist/engine/ 目录,为所有有 .d.ts 文件的模块添加路径。
|
||
#[tauri::command]
|
||
pub fn update_project_tsconfig(app: AppHandle, project_path: String) -> Result<(), String> {
|
||
use std::fs;
|
||
use std::path::Path;
|
||
|
||
let project = Path::new(&project_path);
|
||
if !project.exists() {
|
||
return Err(format!("Project path does not exist: {}", project_path));
|
||
}
|
||
|
||
// Get engine modules path (dist/engine/)
|
||
// 获取引擎模块路径
|
||
let engine_path = get_engine_modules_base_path_internal(&app)?;
|
||
|
||
// Read existing tsconfig.json
|
||
// 读取现有的 tsconfig.json
|
||
let tsconfig_path = project.join("tsconfig.json");
|
||
let tsconfig_editor_path = project.join("tsconfig.editor.json");
|
||
|
||
// Update runtime tsconfig
|
||
// 更新运行时 tsconfig
|
||
if tsconfig_path.exists() {
|
||
update_tsconfig_file(&tsconfig_path, &engine_path, false)?;
|
||
println!("[update_project_tsconfig] Updated {}", tsconfig_path.display());
|
||
}
|
||
|
||
// Update editor tsconfig
|
||
// 更新编辑器 tsconfig
|
||
if tsconfig_editor_path.exists() {
|
||
update_tsconfig_file(&tsconfig_editor_path, &engine_path, true)?;
|
||
println!("[update_project_tsconfig] Updated {}", tsconfig_editor_path.display());
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Internal function to get engine modules base path
|
||
/// 内部函数:获取引擎模块基础路径
|
||
fn get_engine_modules_base_path_internal(app: &AppHandle) -> Result<String, String> {
|
||
let resource_dir = app.path()
|
||
.resource_dir()
|
||
.map_err(|e| format!("Failed to get resource directory: {}", e))?;
|
||
|
||
// Production mode: resource_dir/engine/
|
||
// 生产模式
|
||
let prod_path = resource_dir.join("engine");
|
||
if prod_path.exists() {
|
||
return prod_path.to_str()
|
||
.map(|s| s.to_string())
|
||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||
}
|
||
|
||
// Development mode: workspace/packages/editor-app/dist/engine/
|
||
// 开发模式
|
||
if let Some(ws_root) = find_workspace_root() {
|
||
let dev_path = ws_root.join("packages").join("editor-app").join("dist").join("engine");
|
||
if dev_path.exists() {
|
||
return dev_path.to_str()
|
||
.map(|s| s.to_string())
|
||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||
}
|
||
}
|
||
|
||
Err("Engine modules directory not found".to_string())
|
||
}
|
||
|
||
/// Find workspace root directory
|
||
/// 查找工作区根目录
|
||
fn find_workspace_root() -> Option<std::path::PathBuf> {
|
||
std::env::current_dir()
|
||
.ok()
|
||
.and_then(|cwd| {
|
||
let mut dir = cwd.as_path();
|
||
loop {
|
||
if dir.join("pnpm-workspace.yaml").exists() {
|
||
return Some(dir.to_path_buf());
|
||
}
|
||
match dir.parent() {
|
||
Some(parent) => dir = parent,
|
||
None => return None,
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Update a tsconfig file with engine paths
|
||
/// 使用引擎路径更新 tsconfig 文件
|
||
///
|
||
/// Scans all subdirectories in engine_path for index.d.ts files.
|
||
/// 扫描 engine_path 下所有子目录的 index.d.ts 文件。
|
||
fn update_tsconfig_file(
|
||
tsconfig_path: &std::path::Path,
|
||
engine_path: &str,
|
||
include_editor: bool,
|
||
) -> Result<(), String> {
|
||
use std::fs;
|
||
|
||
let content = fs::read_to_string(tsconfig_path)
|
||
.map_err(|e| format!("Failed to read tsconfig: {}", e))?;
|
||
|
||
let mut config: serde_json::Value = serde_json::from_str(&content)
|
||
.map_err(|e| format!("Failed to parse tsconfig: {}", e))?;
|
||
|
||
// Normalize path for cross-platform compatibility
|
||
// 规范化路径以实现跨平台兼容
|
||
let engine_path_normalized = engine_path.replace('\\', "/");
|
||
|
||
// Build paths mapping by scanning engine modules directory
|
||
// 通过扫描引擎模块目录构建路径映射
|
||
let mut paths = serde_json::Map::new();
|
||
let mut module_count = 0;
|
||
|
||
let engine_dir = std::path::Path::new(engine_path);
|
||
if let Ok(entries) = fs::read_dir(engine_dir) {
|
||
for entry in entries.flatten() {
|
||
let module_path = entry.path();
|
||
if !module_path.is_dir() {
|
||
continue;
|
||
}
|
||
|
||
let module_id = module_path.file_name()
|
||
.and_then(|n| n.to_str())
|
||
.unwrap_or("");
|
||
|
||
// Skip editor modules for runtime tsconfig
|
||
// 运行时 tsconfig 跳过编辑器模块
|
||
if !include_editor && module_id.ends_with("-editor") {
|
||
continue;
|
||
}
|
||
|
||
// Check for index.d.ts
|
||
// 检查是否存在 index.d.ts
|
||
let dts_path = module_path.join("index.d.ts");
|
||
if !dts_path.exists() {
|
||
continue;
|
||
}
|
||
|
||
// Read module.json to get the actual package name
|
||
// 读取 module.json 获取实际的包名
|
||
let module_json_path = module_path.join("module.json");
|
||
let module_name = if module_json_path.exists() {
|
||
fs::read_to_string(&module_json_path)
|
||
.ok()
|
||
.and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
|
||
.and_then(|json| json.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
|
||
.unwrap_or_else(|| format!("@esengine/{}", module_id))
|
||
} else {
|
||
format!("@esengine/{}", module_id)
|
||
};
|
||
|
||
let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id);
|
||
paths.insert(module_name, serde_json::json!([dts_path_str]));
|
||
module_count += 1;
|
||
}
|
||
}
|
||
|
||
println!("[update_tsconfig_file] Found {} modules with type definitions", module_count);
|
||
|
||
// Update compilerOptions.paths
|
||
// 更新 compilerOptions.paths
|
||
if let Some(compiler_options) = config.get_mut("compilerOptions") {
|
||
if let Some(obj) = compiler_options.as_object_mut() {
|
||
obj.insert("paths".to_string(), serde_json::Value::Object(paths));
|
||
// Remove typeRoots since we're using paths
|
||
// 移除 typeRoots,因为我们使用 paths
|
||
obj.remove("typeRoots");
|
||
}
|
||
}
|
||
|
||
// Write back
|
||
// 写回文件
|
||
let output = serde_json::to_string_pretty(&config)
|
||
.map_err(|e| format!("Failed to serialize tsconfig: {}", e))?;
|
||
|
||
fs::write(tsconfig_path, output)
|
||
.map_err(|e| format!("Failed to write tsconfig: {}", e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Start a local HTTP server for runtime preview
|
||
#[tauri::command]
|
||
pub fn start_local_server(root_path: String, port: u16) -> Result<String, String> {
|
||
// If server already running, just return the URL (server persists)
|
||
if SERVER_RUNNING.load(Ordering::SeqCst) {
|
||
return Ok(format!("http://127.0.0.1:{}", port));
|
||
}
|
||
|
||
SERVER_STOP_FLAG.store(false, Ordering::SeqCst);
|
||
SERVER_RUNNING.store(true, Ordering::SeqCst);
|
||
|
||
// Bind to 0.0.0.0 to allow LAN access
|
||
let addr = format!("0.0.0.0:{}", port);
|
||
let server = Server::http(&addr)
|
||
.map_err(|e| format!("Failed to start server: {}", e))?;
|
||
|
||
let root = root_path.clone();
|
||
let stop_flag = Arc::clone(&SERVER_STOP_FLAG);
|
||
|
||
thread::spawn(move || {
|
||
loop {
|
||
if stop_flag.load(Ordering::SeqCst) {
|
||
break;
|
||
}
|
||
|
||
// Use recv_timeout to allow checking stop flag periodically
|
||
match server.recv_timeout(std::time::Duration::from_millis(100)) {
|
||
Ok(Some(request)) => {
|
||
let url = request.url().to_string();
|
||
|
||
// Split URL and query string
|
||
let url_without_query = url.split('?').next().unwrap_or(&url);
|
||
|
||
// Handle different request types
|
||
let file_path = if url.starts_with("/asset?path=") {
|
||
// Asset proxy - extract and decode path parameter
|
||
let query = &url[7..]; // Skip "/asset?"
|
||
if let Some(path_value) = query.strip_prefix("path=") {
|
||
urlencoding::decode(path_value)
|
||
.map(|s| s.to_string())
|
||
.unwrap_or_default()
|
||
} else {
|
||
String::new()
|
||
}
|
||
} else if url_without_query == "/" || url_without_query.is_empty() {
|
||
// Root - serve index.html
|
||
PathBuf::from(&root).join("index.html")
|
||
.to_string_lossy()
|
||
.to_string()
|
||
} else {
|
||
// Static files - remove leading slash and append to root
|
||
let path = url_without_query.trim_start_matches('/');
|
||
PathBuf::from(&root).join(path)
|
||
.to_string_lossy()
|
||
.to_string()
|
||
};
|
||
|
||
println!("[DevServer] Request: {} -> {}", url, file_path);
|
||
|
||
let response = match std::fs::read(&file_path) {
|
||
Ok(content) => {
|
||
let content_type = if file_path.ends_with(".html") {
|
||
"text/html; charset=utf-8"
|
||
} else if file_path.ends_with(".js") {
|
||
"application/javascript"
|
||
} else if file_path.ends_with(".wasm") {
|
||
"application/wasm"
|
||
} else if file_path.ends_with(".css") {
|
||
"text/css"
|
||
} else if file_path.ends_with(".json") {
|
||
"application/json"
|
||
} else if file_path.ends_with(".png") {
|
||
"image/png"
|
||
} else if file_path.ends_with(".jpg") || file_path.ends_with(".jpeg") {
|
||
"image/jpeg"
|
||
} else {
|
||
"application/octet-stream"
|
||
};
|
||
|
||
Response::from_data(content)
|
||
.with_header(
|
||
tiny_http::Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes())
|
||
.unwrap(),
|
||
)
|
||
.with_header(
|
||
tiny_http::Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..])
|
||
.unwrap(),
|
||
)
|
||
}
|
||
Err(_) => Response::from_string("Not Found")
|
||
.with_status_code(404),
|
||
};
|
||
|
||
let _ = request.respond(response);
|
||
}
|
||
Ok(None) => {
|
||
// Timeout, continue loop
|
||
}
|
||
Err(_) => {
|
||
// Error, exit loop
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
SERVER_RUNNING.store(false, Ordering::SeqCst);
|
||
});
|
||
|
||
Ok(format!("http://127.0.0.1:{}", port))
|
||
}
|
||
|
||
/// Stop the local HTTP server
|
||
#[tauri::command]
|
||
pub fn stop_local_server() -> Result<(), String> {
|
||
SERVER_STOP_FLAG.store(true, Ordering::SeqCst);
|
||
Ok(())
|
||
}
|
||
|
||
/// Get local IP address for LAN access
|
||
#[tauri::command]
|
||
pub fn get_local_ip() -> Result<String, String> {
|
||
// Use ipconfig on Windows to get the real LAN IP
|
||
#[cfg(target_os = "windows")]
|
||
{
|
||
let output = Command::new("cmd")
|
||
.args(["/C", "ipconfig"])
|
||
.output()
|
||
.map_err(|e| format!("Failed to run ipconfig: {}", e))?;
|
||
|
||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||
|
||
// Parse ipconfig output to find IPv4 addresses
|
||
let mut found_ips: Vec<String> = Vec::new();
|
||
|
||
for line in output_str.lines() {
|
||
if line.contains("IPv4") || line.contains("IP Address") {
|
||
// Extract IP from line like " IPv4 Address. . . . . . . . . . . : 192.168.1.100"
|
||
if let Some(ip_part) = line.split(':').nth(1) {
|
||
let ip = ip_part.trim().to_string();
|
||
// Prefer 192.168.x.x or 10.x.x.x addresses
|
||
if ip.starts_with("192.168.") || ip.starts_with("10.") {
|
||
return Ok(ip);
|
||
}
|
||
// Collect other IPs as fallback, skip virtual ones
|
||
if !ip.starts_with("172.") && !ip.starts_with("127.") && !ip.starts_with("169.254.") {
|
||
found_ips.push(ip);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Return first non-virtual IP found
|
||
if let Some(ip) = found_ips.first() {
|
||
return Ok(ip.clone());
|
||
}
|
||
}
|
||
|
||
// Fallback for non-Windows or if ipconfig fails
|
||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||
.map_err(|e| format!("Failed to bind socket: {}", e))?;
|
||
|
||
socket.connect("8.8.8.8:80")
|
||
.map_err(|e| format!("Failed to connect: {}", e))?;
|
||
|
||
let local_addr = socket.local_addr()
|
||
.map_err(|e| format!("Failed to get local address: {}", e))?;
|
||
|
||
Ok(local_addr.ip().to_string())
|
||
}
|
||
|
||
/// Generate QR code as base64 PNG
|
||
#[tauri::command]
|
||
pub fn generate_qrcode(text: String) -> Result<String, String> {
|
||
let code = QrCode::new(text.as_bytes())
|
||
.map_err(|e| format!("Failed to create QR code: {}", e))?;
|
||
|
||
let image = code.render::<Luma<u8>>()
|
||
.min_dimensions(200, 200)
|
||
.build();
|
||
|
||
let mut png_data = Vec::new();
|
||
let mut cursor = std::io::Cursor::new(&mut png_data);
|
||
image.write_to(&mut cursor, image::ImageFormat::Png)
|
||
.map_err(|e| format!("Failed to encode PNG: {}", e))?;
|
||
|
||
Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &png_data))
|
||
}
|