feat(app): 支持设置 .md 默认打开方式(docs/32)
三条链路:①tauri.conf bundle.fileAssociations 注册 md/markdown, 新版安装包重装后出现在「打开方式」候选;②文件菜单「设为 .md 默认打开方式…」:Rust 命令经 reg.exe 写 HKCU 的 ProgID NewMarkdown.md/OpenWithProgids/SupportedTypes(免管理员、指向 当前 exe),再打开系统默认应用设置让用户确认一次(Windows 不许 程序直接抢默认);③双击打开:单实例插件转发第二实例参数到待 打开队列+聚焦主窗口,冷启动读 env args,共用 md_files_from_args 过滤;前端 take_pending_open_files 取走逐个打开,只由 main 窗口 消费防子窗口抢文件。命令数 31→33 对账,参数过滤纯函数已抽 crate 实测,vitest 154+2、构建通过;Windows 构建与双击全流程待真机验。
This commit is contained in:
463
src-tauri/Cargo.lock
generated
463
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
encoding_rs = "0.8"
|
||||
|
||||
@@ -941,6 +941,76 @@ pub async fn s3_upload_image(
|
||||
.map_err(|e| format!("上传任务失败:{e}"))?
|
||||
}
|
||||
|
||||
/// 取走待打开文件队列(docs/32):启动参数/第二实例双击转来的 .md 文件。
|
||||
#[tauri::command]
|
||||
pub fn take_pending_open_files(state: tauri::State<'_, crate::PendingOpen>) -> Vec<String> {
|
||||
match state.0.lock() {
|
||||
Ok(mut queue) => std::mem::take(&mut *queue),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 把本程序注册为 .md/.markdown 的候选打开方式(docs/32),并打开 Windows
|
||||
/// 「默认应用」设置页让用户确认。Windows 不允许程序直接抢占默认(UserChoice
|
||||
/// 带校验),应用能做的是注册候选 + 引导用户点一次。
|
||||
#[tauri::command]
|
||||
pub async fn register_file_association() -> Result<String, String> {
|
||||
tauri::async_runtime::spawn_blocking(register_file_association_impl)
|
||||
.await
|
||||
.map_err(|e| format!("注册任务失败:{e}"))?
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn register_file_association_impl() -> Result<String, String> {
|
||||
Err("文件关联设置仅支持 Windows。".into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn register_file_association_impl() -> Result<String, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
let exe = std::env::current_exe().map_err(|e| format!("无法定位程序位置:{e}"))?;
|
||||
let exe_s = exe.to_string_lossy().to_string();
|
||||
let exe_name = exe
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "new-markdown.exe".into());
|
||||
let prog_id = "NewMarkdown.md";
|
||||
let open_cmd = format!("\"{exe_s}\" \"%1\"");
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
let mut reg = |args: &[&str]| {
|
||||
match Command::new("reg.exe")
|
||||
.args(args)
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.status()
|
||||
{
|
||||
Ok(st) if st.success() => {}
|
||||
Ok(st) => failures.push(format!("reg {:?} 退出码 {}", args.get(1), st.code().unwrap_or(-1))),
|
||||
Err(e) => failures.push(format!("reg 执行失败:{e}")),
|
||||
}
|
||||
};
|
||||
// ProgID:类型名、打开命令、图标
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\{prog_id}"), "/ve", "/d", "Markdown 文档", "/f"]);
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\{prog_id}\shell\open\command"), "/ve", "/d", &open_cmd, "/f"]);
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\{prog_id}\DefaultIcon"), "/ve", "/d", &format!("\"{exe_s}\",0"), "/f"]);
|
||||
for ext in [".md", ".markdown"] {
|
||||
// 扩展名的候选列表 + 应用的 SupportedTypes(出现在「打开方式」里)
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\{ext}\OpenWithProgids"), "/v", prog_id, "/t", "REG_SZ", "/d", "", "/f"]);
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\Applications\{exe_name}\SupportedTypes"), "/v", ext, "/t", "REG_SZ", "/d", "", "/f"]);
|
||||
}
|
||||
reg(&["add", &format!(r"HKCU\Software\Classes\Applications\{exe_name}\shell\open\command"), "/ve", "/d", &open_cmd, "/f"]);
|
||||
if !failures.is_empty() {
|
||||
return Err(format!("注册表写入失败:{}", failures.join(";")));
|
||||
}
|
||||
// 打开系统「默认应用」设置,用户在那里把 .md 选成本程序(只需一次)
|
||||
let _ = Command::new("explorer.exe")
|
||||
.arg("ms-settings:defaultapps")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn();
|
||||
Ok("已把 new-markdown 注册为 .md / .markdown 的候选打开方式,并打开了 Windows「默认应用」设置。请在设置里找到 .md,把默认应用选为 new-markdown(Windows 要求手动确认一次)。".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn s3_upload_base64(
|
||||
config: crate::s3::S3Config,
|
||||
|
||||
@@ -4,11 +4,28 @@ mod search;
|
||||
mod storage;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
/// 应用数据库句柄(阶段 4):连接在启动时打开并建表,命令层经 State 访问。
|
||||
pub struct AppDb(pub Mutex<rusqlite::Connection>);
|
||||
|
||||
/// 待打开文件队列(docs/32):启动参数与第二实例转发的 .md 文件先存这里,
|
||||
/// 前端就绪后经 take_pending_open_files 命令取走打开。
|
||||
pub struct PendingOpen(pub Mutex<Vec<String>>);
|
||||
|
||||
/// 从命令行参数里挑出要打开的 Markdown 文件:跳过开关项,只收扩展名
|
||||
/// .md/.markdown 且确实存在的文件。
|
||||
pub fn md_files_from_args<I: IntoIterator<Item = String>>(args: I) -> Vec<String> {
|
||||
args.into_iter()
|
||||
.filter(|a| !a.starts_with('-'))
|
||||
.filter(|a| {
|
||||
let lower = a.to_lowercase();
|
||||
(lower.ends_with(".md") || lower.ends_with(".markdown"))
|
||||
&& std::path::Path::new(a).is_file()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 打开数据库并建表:WAL 降低并发写冲突,busy_timeout 让多开实例的写操作排队而非直接报错。
|
||||
fn open_and_init(path: &std::path::Path) -> Result<rusqlite::Connection, String> {
|
||||
let conn = rusqlite::Connection::open(path).map_err(|e| e.to_string())?;
|
||||
@@ -22,6 +39,23 @@ fn open_and_init(path: &std::path::Path) -> Result<rusqlite::Connection, String>
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
// 单实例必须最先注册:双击第二个 .md 时不再起新进程,把文件转给
|
||||
// 已运行的窗口打开(docs/32)。
|
||||
.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
|
||||
let files = md_files_from_args(argv);
|
||||
if !files.is_empty() {
|
||||
if let Some(state) = app.try_state::<PendingOpen>() {
|
||||
if let Ok(mut queue) = state.0.lock() {
|
||||
queue.extend(files);
|
||||
}
|
||||
}
|
||||
let _ = app.emit("pending-open-files", ());
|
||||
}
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.unminimize();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
let data_dir = app.path().app_data_dir().expect("app data dir");
|
||||
@@ -50,6 +84,9 @@ pub fn run() {
|
||||
}
|
||||
};
|
||||
app.manage(AppDb(Mutex::new(conn)));
|
||||
// 冷启动参数(双击 .md 打开本程序时,文件路径在这里)
|
||||
let initial = md_files_from_args(std::env::args().skip(1));
|
||||
app.manage(PendingOpen(Mutex::new(initial)));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -70,6 +107,8 @@ pub fn run() {
|
||||
commands::s3_upload_image,
|
||||
commands::s3_upload_base64,
|
||||
commands::s3_test_connection,
|
||||
commands::take_pending_open_files,
|
||||
commands::register_file_association,
|
||||
commands::delete_to_trash,
|
||||
commands::db_recent_list,
|
||||
commands::db_recent_add,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"$schema":"https://schema.tauri.app/config/2","app":{"security":{"csp":null},"windows":[{"center":true,"decorations":false,"height":820,"label":"main","minHeight":600,"minWidth":920,"resizable":true,"title":"new-markdown","width":1280,"dragDropEnabled":false}]},"build":{"beforeBuildCommand":"npm run build","beforeDevCommand":"npm run dev","devUrl":"http://localhost:1420","frontendDist":"../dist"},"bundle":{"active":true,"category":"Productivity","windows":{"nsis":{"languages":["SimpChinese"],"displayLanguageSelector":false},"wix":{"language":"zh-CN"}},"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.ico","icons/icon.png"],"shortDescription":"Windows Markdown 所见即所得编辑器","targets":["nsis","msi"]},"identifier":"app.newmarkdown.desktop","productName":"new-markdown","version":"0.1.0"}
|
||||
{"$schema":"https://schema.tauri.app/config/2","app":{"security":{"csp":null},"windows":[{"center":true,"decorations":false,"height":820,"label":"main","minHeight":600,"minWidth":920,"resizable":true,"title":"new-markdown","width":1280,"dragDropEnabled":false}]},"build":{"beforeBuildCommand":"npm run build","beforeDevCommand":"npm run dev","devUrl":"http://localhost:1420","frontendDist":"../dist"},"bundle":{"active":true,"category":"Productivity","windows":{"nsis":{"languages":["SimpChinese"],"displayLanguageSelector":false},"wix":{"language":"zh-CN"}},"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.ico","icons/icon.png"],"shortDescription":"Windows Markdown 所见即所得编辑器","targets":["nsis","msi"],"fileAssociations":[{"ext":["md","markdown"],"name":"NewMarkdown","description":"Markdown 文档","role":"Editor"}]},"identifier":"app.newmarkdown.desktop","productName":"new-markdown","version":"0.1.0"}
|
||||
Reference in New Issue
Block a user