Skip to content

Commit 5814a19

Browse files
authored
Merge pull request #1 from Silwings-git/v1.0.4
编组级快捷方式,自定义启动参数与bug修复
2 parents 21775bc + 7a5dfeb commit 5814a19

File tree

14 files changed

+806
-178
lines changed

14 files changed

+806
-178
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "one-click-launch",
33
"private": true,
4-
"version": "1.0.3",
4+
"version": "1.0.4",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "one-click-launch"
3-
version = "1.0.3"
3+
version = "1.0.4"
44
description = "One Click Launch"
55
authors = ["Silwings"]
66
edition = "2024"
@@ -31,14 +31,16 @@ serde = { version = "1", features = ["derive"] }
3131
serde_json = "1"
3232
thiserror = "2.0.3"
3333
anyhow = "1.0.93"
34-
tracing = "0.1" # 日志处理
35-
tracing-subscriber = "0.3" # 日志处理
34+
tracing = "0.1"
35+
tracing-subscriber = "0.3"
3636
windows = "0.58.0"
3737
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite"] }
3838
tokio = { version = "1.43.0", features = ["full"] }
3939
rand = "0.8.5"
4040
lazy_static = "1.5.0"
4141
itertools = "0.14.0"
42+
dirs = "5.0.1"
43+
shlex = "1.3.0"
4244

4345
[features]
4446
portable = []

src-tauri/src/api/launcher_api.rs

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
use std::{path::Path, process::Command};
2+
13
use anyhow::Result;
24
use rand::{Rng, distributions::Alphanumeric};
35
use serde::Deserialize;
4-
use tauri::{AppHandle, State};
6+
use tauri::{AppHandle, Manager, State};
57
use tauri_plugin_opener::OpenerExt;
68
use tracing::info;
79

810
use crate::{
911
DatabaseManager,
12+
api::window_api,
1013
db::{
1114
launcher,
1215
launcher_resource::{self, CreateResourceParam, LauncherResource},
@@ -283,6 +286,17 @@ pub async fn modify_resource_name(
283286
Ok(())
284287
}
285288

289+
/// 修改资源路径
290+
#[tauri::command]
291+
pub async fn modify_resource_path(
292+
db: State<'_, DatabaseManager>,
293+
resource_id: i64,
294+
path: &str,
295+
) -> Result<(), OneClickLaunchError> {
296+
launcher_resource::modify_path(&db.pool, resource_id, path).await?;
297+
Ok(())
298+
}
299+
286300
/// 删除启动器中的资源
287301
#[tauri::command]
288302
pub async fn delete_resource(
@@ -295,12 +309,26 @@ pub async fn delete_resource(
295309

296310
/// 启动启动器
297311
#[tauri::command]
298-
pub async fn launch(
299-
app: AppHandle,
300-
db: State<'_, DatabaseManager>,
301-
launcher_id: i64,
302-
) -> Result<(), OneClickLaunchError> {
303-
let resources = launcher_resource::query_by_launcher_id(&db.pool, launcher_id).await?;
312+
pub async fn launch(app: AppHandle, launcher_id: i64) -> Result<(), OneClickLaunchError> {
313+
let db: State<'_, DatabaseManager> = app.try_state().ok_or(
314+
OneClickLaunchError::ExecutionError("Unable to get DatabaseManager".to_string()),
315+
)?;
316+
317+
let mut resources = launcher_resource::query_by_launcher_id(&db.pool, launcher_id).await?;
318+
319+
tracing::debug!("启动编组原始资源列表: {resources:?}");
320+
321+
// 必须从启动资源中排除自己,防止出现死循环
322+
let app_path = current_exe_path_str()?;
323+
resources.retain(|e| {
324+
// 检查路径是否指向当前应用程序
325+
!e.path.starts_with(&app_path)
326+
});
327+
328+
if resources.is_empty() {
329+
tracing::debug!("资源列表为空");
330+
return Ok(());
331+
}
304332

305333
launch_resources(&app, &resources);
306334

@@ -342,8 +370,78 @@ pub fn launch_resources(app: &AppHandle, resources: &[LauncherResource]) {
342370
/// - `Ok(())` 表示操作成功。
343371
/// - `Err(OneClickLaunchError)` 表示操作失败。
344372
pub fn open_using_default_program(app: &AppHandle, path: &str) -> Result<(), OneClickLaunchError> {
373+
match try_open_as_command(path) {
374+
Ok(true) => return Ok(()), // 已经作为程序执行
375+
Ok(false) => {
376+
// 不是程序,交给 opener
377+
open_path_with_opener(app, path)?;
378+
}
379+
Err(e) => {
380+
tracing::debug!("作为命令执行失败: {e:?},尝试默认打开");
381+
open_path_with_opener(app, path)?;
382+
}
383+
}
384+
385+
Ok(())
386+
}
387+
388+
fn open_path_with_opener(app: &AppHandle, path: &str) -> Result<(), OneClickLaunchError> {
345389
app.opener()
346390
.open_path(path, None::<&str>)
347391
.map_err(|e| OneClickLaunchError::ExecutionError(e.to_string()))?;
348392
Ok(())
349393
}
394+
395+
fn try_open_as_command(path: &str) -> Result<bool, OneClickLaunchError> {
396+
let parts = shlex::split(path)
397+
.ok_or_else(|| OneClickLaunchError::ExecutionError("无法解析路径".to_string()))?;
398+
399+
if parts.is_empty() {
400+
return Ok(false);
401+
}
402+
403+
let program = &parts[0];
404+
let args = &parts[1..];
405+
406+
// 如果第一个部分是存在的文件(.exe/.bat/.sh 等),才当成命令执行
407+
if Path::new(program).exists() {
408+
Command::new(program).args(args).spawn()?;
409+
return Ok(true);
410+
}
411+
412+
Ok(false)
413+
}
414+
415+
#[tauri::command]
416+
pub async fn create_handler_shortcut(
417+
launcher_id: i64,
418+
db: State<'_, DatabaseManager>,
419+
) -> Result<String, OneClickLaunchError> {
420+
let launcher = launcher::find_by_id(&db.pool, launcher_id).await?;
421+
422+
let app_path = current_exe_path_str()?;
423+
424+
// 构建参数
425+
let args = Some(vec![format!("launch {}", launcher_id)]);
426+
427+
window_api::create_shortcut(
428+
&app_path,
429+
&launcher.name,
430+
args,
431+
// None 表示保存到桌面
432+
None,
433+
)
434+
.map(|path| path.to_string_lossy().to_string())
435+
}
436+
437+
fn current_exe_path_str() -> Result<String, OneClickLaunchError> {
438+
// 获取当前应用程序的绝对路径
439+
let exe_path = std::env::current_exe()?;
440+
441+
// 转换为 Windows 可识别的普通路径
442+
let mut app_path = exe_path.to_string_lossy().to_string();
443+
if app_path.starts_with(r"\\?\") {
444+
app_path = app_path.trim_start_matches(r"\\?\").to_string();
445+
}
446+
Ok(app_path)
447+
}

src-tauri/src/api/window_api.rs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::{sync::Mutex, time::Instant};
22

3-
use anyhow::Result;
43
use tauri::{
54
AppHandle, DragDropEvent, Manager, State, Theme,
65
menu::{MenuBuilder, MenuItem},
@@ -104,7 +103,7 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
104103

105104
if let Ok(ref mut last_reset) = lock {
106105
// 500ms防抖间隔
107-
if last_reset.map_or(true, |t| now.duration_since(t).as_millis() > 500) {
106+
if last_reset.is_none_or(|t| now.duration_since(t).as_millis() > 500) {
108107
if let Ok(physical_size) = window.inner_size() {
109108
// 如果窗口大小异常,强制调整到正常大小
110109
if physical_size.width != WINDOW_MIN_WIDTH
@@ -140,7 +139,7 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
140139
}
141140

142141
/// 初始化窗口
143-
pub fn setup_tray(app: &AppHandle) -> Result<()> {
142+
pub fn setup_tray(app: &AppHandle) -> Result<(), OneClickLaunchError> {
144143
let tray_icon = TrayIconBuilder::new()
145144
// 设置系统托盘的提示,鼠标悬浮时会显示
146145
.tooltip(constants::APPLICATION_NAME)
@@ -179,9 +178,7 @@ pub fn setup_tray(app: &AppHandle) -> Result<()> {
179178
if let Ok(launcher_id) = id.parse::<i64>() {
180179
let app_cloned = app.clone();
181180
tauri::async_runtime::spawn(async move {
182-
let inner_app = app_cloned.clone();
183-
let db = inner_app.state();
184-
launcher_api::launch(app_cloned, db, launcher_id).await
181+
launcher_api::launch(app_cloned, launcher_id).await
185182
});
186183
}
187184
}
@@ -202,3 +199,74 @@ pub fn setup_tray(app: &AppHandle) -> Result<()> {
202199
pub struct ScaleFactorChangedState {
203200
pub last_reset: Mutex<Option<Instant>>,
204201
}
202+
203+
use std::os::windows::ffi::OsStrExt;
204+
use std::path::PathBuf;
205+
use windows::{
206+
Win32::{
207+
System::Com::{
208+
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
209+
CoUninitialize, IPersistFile,
210+
},
211+
UI::Shell::{IShellLinkW, ShellLink},
212+
},
213+
core::{HSTRING, Interface, PCWSTR},
214+
};
215+
216+
/// 创建 Windows 快捷方式 (.lnk 文件)
217+
pub fn create_shortcut(
218+
app_path: &str,
219+
shortcut_name: &str,
220+
args: Option<Vec<String>>,
221+
target_dir: Option<&str>,
222+
) -> Result<PathBuf, OneClickLaunchError> {
223+
unsafe {
224+
// 初始化 COM
225+
CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()?;
226+
227+
// 创建 IShellLink 实例
228+
let shell_link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
229+
230+
// 设置应用路径
231+
shell_link.SetPath(&HSTRING::from(dbg!(app_path)))?;
232+
233+
// 设置参数
234+
if let Some(arguments) = args {
235+
let arg_str = arguments.join(" ");
236+
shell_link.SetArguments(&HSTRING::from(arg_str))?;
237+
}
238+
239+
// 设置工作目录(使用 app_path 的父目录)
240+
if let Some(parent) = std::path::Path::new(app_path).parent() {
241+
shell_link.SetWorkingDirectory(&HSTRING::from(parent.to_string_lossy().to_string()))?;
242+
}
243+
244+
// 获取 IPersistFile 接口
245+
let persist_file: IPersistFile = shell_link.cast()?;
246+
247+
// 目标目录(默认桌面)
248+
let save_dir = if let Some(dir) = target_dir {
249+
PathBuf::from(dir)
250+
} else {
251+
dirs::desktop_dir().ok_or_else(|| anyhow::anyhow!("无法获取桌面路径"))?
252+
};
253+
254+
// 拼接快捷方式路径
255+
let lnk_path = save_dir.join(format!("{}.lnk", shortcut_name));
256+
257+
// 转换为宽字符串
258+
let wide: Vec<u16> = lnk_path
259+
.as_os_str()
260+
.encode_wide()
261+
.chain(std::iter::once(0))
262+
.collect();
263+
264+
// 保存
265+
persist_file.Save(PCWSTR::from_raw(wide.as_ptr()), true)?;
266+
267+
// 释放 COM
268+
CoUninitialize();
269+
270+
Ok(lnk_path)
271+
}
272+
}

src-tauri/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use lazy_static::lazy_static;
22

33
lazy_static! {
44
pub static ref AUTO_START_FLAG: String = "--auto".to_string();
5+
pub static ref LAUNCH_SPECIFIED_LAUNCHER_KEY: String = "launch".to_string();
56
}
67

78
pub static APPLICATION_NAME: &str = "一键启动";

src-tauri/src/db/launcher_resource.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ where
8585
Ok(())
8686
}
8787

88+
// 修改路径
89+
pub async fn modify_path<'a, E>(executor: E, resource_id: i64, path: &str) -> Result<()>
90+
where
91+
E: Executor<'a, Database = Sqlite>,
92+
{
93+
sqlx::query("UPDATE launcher_resource SET path = ? WHERE id = ?")
94+
.bind(path)
95+
.bind(resource_id)
96+
.execute(executor)
97+
.await?;
98+
Ok(())
99+
}
100+
88101
// 按launcher_id删除
89102
pub async fn delete_by_launcher<'a, E>(executor: E, launcher_id: i64) -> Result<()>
90103
where

src-tauri/src/error.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ pub enum OneClickLaunchError {
1414
AnyhowError(#[from] anyhow::Error),
1515

1616
#[error("{0}")]
17-
InfoError(#[from] std::io::Error),
17+
IOError(#[from] std::io::Error),
1818

1919
#[error("{0}")]
2020
TauriError(#[from] tauri::Error),
2121

2222
#[error("Unable to convert from {0} to Event")]
2323
EventConvertError(String),
24+
25+
#[error("{0}")]
26+
WindowsCommonError(String),
27+
28+
#[error("{0}")]
29+
WindowsError(#[from] windows::core::Error),
2430
}
2531

2632
// we must manually implement serde::Serialize

0 commit comments

Comments
 (0)