Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ windows = { version = "0.61.3", features = [
"Win32_Security",
"Win32_UI_Shell"
] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
semver = "1"
self-replace = "1.5"
tempfile = "3"

[target."cfg(target_os = \"macos\")".dependencies]
rdev = "0.5.3"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/gen/schemas/acl-manifests.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-tauri/permissions/dmnote-allow-all.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"commands": {
"allow": [
"app_bootstrap",
"app_auto_update",
"app_open_external",
"app_restart",
"app_quit",
Expand Down
60 changes: 36 additions & 24 deletions src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,7 @@ impl AppState {
true,
None::<&str>,
)?;
let quit_item = MenuItem::with_id(
app,
TRAY_MENU_QUIT_ID,
quit_label,
true,
None::<&str>,
)?;
let quit_item = MenuItem::with_id(app, TRAY_MENU_QUIT_ID, quit_label, true, None::<&str>)?;
let menu = Menu::with_items(app, &[&settings_item, &quit_item])?;
let overlay_force_close = self.overlay_force_close.clone();

Expand Down Expand Up @@ -202,6 +196,7 @@ impl AppState {
laboratory_enabled: state.laboratory_enabled,
developer_mode_enabled: state.developer_mode_enabled,
tray_enabled: state.tray_enabled,
auto_update_enabled: state.auto_update_enabled,
background_color: state.background_color.clone(),
use_custom_css: state.use_custom_css,
custom_css: state.custom_css.clone(),
Expand Down Expand Up @@ -239,7 +234,10 @@ impl AppState {
}

pub fn emit_settings_changed(&self, diff: &SettingsDiff, app: &AppHandle) -> Result<()> {
log::debug!("[IPC] emit_settings_changed: {} fields changed", diff.changed_count());
log::debug!(
"[IPC] emit_settings_changed: {} fields changed",
diff.changed_count()
);
self.apply_settings_effects(diff, app)?;
if let Some(value) = diff.changed.key_counter_enabled {
self.key_counter_enabled.store(value, Ordering::SeqCst);
Expand All @@ -253,13 +251,13 @@ impl AppState {

pub fn set_overlay_visibility(&self, app: &AppHandle, visible: bool) -> Result<()> {
log::debug!("[IPC] set_overlay_visibility: visible={}", visible);

if visible {
// 오버레이를 열 때: 창이 없으면 생성하고 표시
let window = self.ensure_overlay_window(app)?;
let snapshot = self.store.snapshot();
show_overlay_window(&window, snapshot.always_on_top)?;

// 오버레이가 숨겨진 동안 변경된 설정을 다시 적용
window.set_ignore_cursor_events(snapshot.overlay_locked)?;
window.set_always_on_top(snapshot.always_on_top)?;
Expand All @@ -272,14 +270,18 @@ impl AppState {
hide_overlay_window(&window)?;
}
}

*self.overlay_visible.write() = visible;
app.emit("overlay:visibility", &json!({ "visible": visible }))?;
Ok(())
}

pub fn set_overlay_lock(&self, app: &AppHandle, locked: bool, persist: bool) -> Result<()> {
log::debug!("[IPC] set_overlay_lock: locked={}, persist={}", locked, persist);
log::debug!(
"[IPC] set_overlay_lock: locked={}, persist={}",
locked,
persist
);
if persist {
let _ = self.store.update(|state| {
state.overlay_locked = locked;
Expand Down Expand Up @@ -344,7 +346,8 @@ impl AppState {
) -> Result<OverlayBounds> {
// 오버레이가 이미 열려있을 때만 리사이즈 수행
// 창이 없으면 에러 반환 (창을 자동으로 생성하지 않음)
let window = app.get_webview_window(OVERLAY_LABEL)
let window = app
.get_webview_window(OVERLAY_LABEL)
.ok_or_else(|| anyhow!("Overlay window is not open"))?;
let anchor = anchor
.and_then(|value| overlay_resize_anchor_from_str(&value))
Expand Down Expand Up @@ -429,7 +432,10 @@ impl AppState {

log::debug!(
"[IPC] resize_overlay: emit overlay:resized ({}x{} at {}, {})",
bounds.width, bounds.height, bounds.x, bounds.y
bounds.width,
bounds.height,
bounds.x,
bounds.y
);
app.emit(
"overlay:resized",
Expand Down Expand Up @@ -461,12 +467,17 @@ impl AppState {
let pipe_receiver: Option<std::sync::mpsc::Receiver<Option<std::fs::File>>> = {
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
match crate::ipc::pipe_server_create("dmnote_keys_v1") {
Ok(f) => { let _ = tx.send(Some(f)); }
Err(err) => { warn!("failed to create named pipe: {err}"); let _ = tx.send(None); }
}
});
std::thread::spawn(
move || match crate::ipc::pipe_server_create("dmnote_keys_v1") {
Ok(f) => {
let _ = tx.send(Some(f));
}
Err(err) => {
warn!("failed to create named pipe: {err}");
let _ = tx.send(None);
}
},
);
Some(rx)
};
#[cfg(not(target_os = "windows"))]
Expand Down Expand Up @@ -998,7 +1009,7 @@ impl AppState {
fn apply_settings_effects(&self, diff: &SettingsDiff, app: &AppHandle) -> Result<()> {
// 오버레이가 보이는 상태일 때만 설정 적용
let is_visible = *self.overlay_visible.read();

if let Some(value) = diff.changed.always_on_top {
if is_visible {
if let Some(window) = app.get_webview_window(OVERLAY_LABEL) {
Expand All @@ -1021,7 +1032,7 @@ impl AppState {
}

if let Some(enabled) = diff.changed.developer_mode_enabled {
// 활성화 시에만 DevTools 열기
// 활성화 시에만 DevTools 열기
if enabled {
if let Some(main) = app.get_webview_window("main") {
let _ = main.open_devtools();
Expand All @@ -1036,7 +1047,9 @@ impl AppState {
if !enabled {
let _ = app.remove_tray_by_id(TRAY_ICON_ID);
if let Err(err) = self.set_main_window_hidden(false) {
log::warn!("failed to clear main_window_hidden when disabling tray mode: {err}");
log::warn!(
"failed to clear main_window_hidden when disabling tray mode: {err}"
);
}
} else if self.store.snapshot().main_window_hidden {
self.ensure_tray_icon_for_background(app)?;
Expand Down Expand Up @@ -1691,4 +1704,3 @@ struct OverlayPosition {
x: f64,
y: f64,
}

3 changes: 2 additions & 1 deletion src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pub mod overlay;
pub mod plugin_storage;
pub mod preset;
pub mod settings;
pub mod system;
pub mod stat_items;
pub mod system;
pub mod update;
106 changes: 106 additions & 0 deletions src-tauri/src/commands/update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use tauri::AppHandle;

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AutoUpdateResult {
pub previous_version: String,
pub updated_to: String,
pub download_url: String,
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn app_auto_update(app: AppHandle, tag: String) -> Result<AutoUpdateResult, String> {
#[cfg(target_os = "windows")]
{
return app_auto_update_windows(app, &tag);
}

#[cfg(not(target_os = "windows"))]
{
let _ = app;
let _ = tag;
Err("auto update is only supported on Windows".to_string())
}
}

#[cfg(target_os = "windows")]
fn app_auto_update_windows(app: AppHandle, tag: &str) -> Result<AutoUpdateResult, String> {
use std::time::Duration;

const REPO_OWNER: &str = "lee-sihun";
const REPO_NAME: &str = "DmNote";
const ASSET_NAME: &str = "DM.NOTE.exe";

let trimmed_tag = tag.trim();
if trimmed_tag.is_empty() {
return Err("update tag is empty".to_string());
}
if !is_safe_tag(trimmed_tag) {
return Err("update tag contains unsupported characters".to_string());
}

let previous_version = app.package_info().version.to_string();
let current_version = parse_semver_version(&previous_version)?;
let target_version = parse_semver_version(trimmed_tag)?;
if target_version <= current_version {
return Err(format!(
"target version ({target_version}) must be newer than current version ({current_version})"
));
}

let download_url = format!(
"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/download/{trimmed_tag}/{ASSET_NAME}"
);

let client = reqwest::blocking::Client::builder()
.user_agent("dm-note-auto-updater")
.timeout(Duration::from_secs(180))
.build()
.map_err(|err| format!("failed to initialize downloader: {err}"))?;

let response = client
.get(&download_url)
.send()
.map_err(|err| format!("failed to download update asset: {err}"))?;
let response = response
.error_for_status()
.map_err(|err| format!("failed to download update asset: {err}"))?;
let bytes = response
.bytes()
.map_err(|err| format!("failed to read downloaded update: {err}"))?;
if bytes.is_empty() {
return Err("downloaded update file is empty".to_string());
}

let temp_dir = tempfile::Builder::new()
.prefix("dmnote-update-")
.tempdir()
.map_err(|err| format!("failed to create temporary directory: {err}"))?;
let temp_exe = temp_dir.path().join(ASSET_NAME);
std::fs::write(&temp_exe, &bytes)
.map_err(|err| format!("failed to write update file: {err}"))?;

self_replace::self_replace(&temp_exe)
.map_err(|err| format!("failed to replace executable: {err}"))?;

app.request_restart();

Ok(AutoUpdateResult {
previous_version,
updated_to: target_version.to_string(),
download_url,
})
}

#[cfg(target_os = "windows")]
fn parse_semver_version(raw: &str) -> Result<semver::Version, String> {
let normalized = raw.trim().trim_start_matches(['v', 'V']);
semver::Version::parse(normalized)
.map_err(|err| format!("invalid version string '{raw}': {err}"))
}

#[cfg(target_os = "windows")]
fn is_safe_tag(tag: &str) -> bool {
tag.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | '+'))
}
Loading