feat: shortcuts

This commit is contained in:
2025-04-16 13:20:43 +00:00
parent d3145bfc4e
commit e6c21ef04a
10 changed files with 83 additions and 3 deletions

BIN
res/icon-chunithm.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
res/icon-ongeki.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

2
rust/Cargo.lock generated
View File

@ -803,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@ -53,5 +53,5 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-updater = "2"
[target.'cfg(target_os = "windows")'.dependencies]
winsafe = { version = "0.0.23", features = ["user"] }
winsafe = { version = "0.0.23", features = ["user", "ole", "shell"] }
displayz = "^0.2.0"

View File

@ -397,6 +397,13 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
Ok(())
}
#[tauri::command]
pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
log::debug!("invoke: create_shortcut({:?})", profile_meta);
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities");

View File

@ -199,6 +199,7 @@ pub async fn run(_args: Vec<String>) {
cmd::sync_current_profile,
cmd::save_current_profile,
cmd::load_segatools_ini,
cmd::create_shortcut,
cmd::get_global_config,
cmd::set_global_config,

View File

@ -20,6 +20,13 @@ impl Game {
}
}
pub fn print(&self) -> &'static str {
match self {
Game::Ongeki => "O.N.G.E.K.I.",
Game::Chunithm => "CHUNITHM"
}
}
pub fn hook_exe(&self) -> &'static str {
match self {
Game::Ongeki => "mu3hook.dll",

View File

@ -173,4 +173,44 @@ pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
} else {
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
}
}
#[cfg(target_os = "windows")]
pub fn create_shortcut(
apph: AppHandle,
meta: &crate::profiles::ProfileMeta
) -> Result<()> {
use winsafe::{co, prelude::{ole_IPersistFile, ole_IUnknown, shell_IShellLink}, CoCreateInstance, CoInitializeEx, IPersistFile};
let _com_guard = CoInitializeEx(
co::COINIT::APARTMENTTHREADED
| co::COINIT::DISABLE_OLE1DDE,
)?;
let obj = CoCreateInstance::<winsafe::IShellLink>(
&co::CLSID::ShellLink,
None,
co::CLSCTX::INPROC_SERVER,
)?;
let target_dir = apph.path().cache_dir()?.join(NAME);
let target_path = target_dir.join("startliner.exe");
let lnk_path = apph.path().desktop_dir()?.join(format!("{}-{}.lnk", &meta.game, &meta.name));
obj.SetPath(target_path.to_str().ok_or_else(|| anyhow!("Illegal target path"))?)?;
obj.SetDescription(&format!("{} {}", &meta.game.print(), &meta.name))?;
obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?;
obj.SetIconLocation(
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
0
)?;
match meta.game {
Game::Ongeki => std::fs::write(target_dir.join("icon-ongeki.ico"), include_bytes!("../../res/icon-ongeki.ico")),
Game::Chunithm => std::fs::write(target_dir.join("icon-chunithm.ico"), include_bytes!("../../res/icon-chunithm.ico"))
}?;
let file = obj.QueryInterface::<IPersistFile>()?;
file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
Ok(())
}

View File

@ -2,6 +2,7 @@
import { ref } from 'vue';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import { useConfirm } from 'primevue/useconfirm';
import * as path from '@tauri-apps/api/path';
import { invoke } from '../invoke';
import { useGeneralStore, usePrfStore } from '../stores';
@ -9,6 +10,8 @@ import { ProfileMeta } from '../types';
const general = useGeneralStore();
const prf = usePrfStore();
const confirmDialog = useConfirm();
const isEditing = ref(false);
const props = defineProps({
@ -54,6 +57,14 @@ const deleteProfile = async () => {
await prf.reloadList();
await prf.reload();
};
const promptDeleteProfile = async () => {
confirmDialog.require({
message: `Are you sure you want to delete ${props.p?.game}-${props.p?.name}?`,
header: 'Delete profile',
accept: deleteProfile,
});
};
</script>
<template>
@ -90,7 +101,7 @@ const deleteProfile = async () => {
size="small"
class="self-center ml-2"
style="width: 2rem; height: 2rem"
@click="deleteProfile"
@click="promptDeleteProfile"
/>
<Button
rounded

View File

@ -85,6 +85,15 @@ listen('launch-end', () => {
getCurrentWindow().setFocus();
});
const createShortcut = async () => {
const current = prf.current;
if (current !== null) {
await invoke('create_shortcut', {
profileMeta: current.meta,
});
}
};
const menuItems = [
{
label: 'Refresh and start',
@ -96,6 +105,11 @@ const menuItems = [
icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false),
},
{
label: 'Create shortcut',
icon: 'pi pi-link',
command: createShortcut,
},
];
const menu = ref();