From fe108a3b61db4de431bfe89b6041c5a91000a290 Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 23 Apr 2026 11:21:38 +0200 Subject: [PATCH] design: upgrade global design of the app --- src-tauri/capabilities/default.json | 5 +- src-tauri/src/commands.rs | 58 +++++++++++++++++++++- src-tauri/src/lib.rs | 1 + src/App.tsx | 25 +++++++++- src/components/ImageViewerWindow.tsx | 74 ++++++++++++++++++++++++++++ src/components/QuestDetailPanel.tsx | 14 ++++-- src/components/ResizeHandles.tsx | 31 ++++++++++++ src/components/TitleBar.tsx | 5 ++ src/main.tsx | 5 +- 9 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 src/components/ImageViewerWindow.tsx create mode 100644 src/components/ResizeHandles.tsx diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 11422cc..a386d89 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": ["main", "image-viewer"], "permissions": [ "core:default", "opener:default", @@ -21,6 +21,7 @@ "core:window:allow-set-cursor-visible", "core:window:allow-is-maximized", "core:window:allow-is-minimized", - "core:window:allow-is-focused" + "core:window:allow-is-focused", + "core:window:allow-start-resize-dragging" ] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 8512f31..23dfa13 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,4 +1,5 @@ -use tauri::{AppHandle, Manager, State}; +use tauri::{AppHandle, Emitter, Manager, State}; +use tauri::window::Color; use serde::{Deserialize, Serialize}; use std::sync::Mutex; use rusqlite::Connection; @@ -494,6 +495,61 @@ pub fn toggle_quest_step( db::toggle_quest_step(&conn, &profile_id, &quest_name, step_index).map_err(|e| e.to_string()) } +fn percent_encode(s: &str) -> String { + let mut result = String::new(); + for byte in s.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + result.push(byte as char); + } + b => result.push_str(&format!("%{:02X}", b)), + } + } + result +} + +#[tauri::command] +pub async fn open_image_viewer( + app: AppHandle, + state: State<'_, DbState>, + image_url: String, +) -> Result<(), String> { + if let Some(win) = app.get_webview_window("image-viewer") { + win.emit("set-viewer-image", &image_url).map_err(|e| e.to_string())?; + win.set_focus().map_err(|e| e.to_string())?; + return Ok(()); + } + + let (w, h, x, y) = { + let conn = state.0.lock().map_err(|e| e.to_string())?; + let w: f64 = db::get_setting(&conn, "viewer_width").and_then(|v| v.parse().ok()).unwrap_or(600.0); + let h: f64 = db::get_setting(&conn, "viewer_height").and_then(|v| v.parse().ok()).unwrap_or(500.0); + let x: Option = db::get_setting(&conn, "viewer_x").and_then(|v| v.parse().ok()); + let y: Option = db::get_setting(&conn, "viewer_y").and_then(|v| v.parse().ok()); + (w, h, x, y) + }; + + let path = format!("/?viewer=1&imageUrl={}", percent_encode(&image_url)); + let mut builder = tauri::WebviewWindowBuilder::new( + &app, + "image-viewer", + tauri::WebviewUrl::App(path.into()), + ) + .title("Image") + .decorations(false) + .resizable(true) + .always_on_top(true) + .background_color(Color(13, 17, 23, 255)) + .inner_size(w, h); + + if let (Some(x), Some(y)) = (x, y) { + builder = builder.position(x, y); + } + + builder.build().map_err(|e| e.to_string())?; + Ok(()) +} + fn collect_quest_names(data: &parser::GuideData) -> Vec { let mut names = Vec::new(); for section in &data.sections { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 49a6c29..815fc70 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -53,6 +53,7 @@ pub fn run() { commands::toggle_quest_step, commands::get_resource_inventory, commands::set_resource_quantity, + commands::open_image_viewer, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index 4a16fd7..d88bf3b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; +import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { useStore } from "./store"; import TitleBar from "./components/TitleBar"; +import ResizeHandles from "./components/ResizeHandles"; import HomeView from "./components/HomeView"; import GuideView from "./components/GuideView"; import SettingsPanel from "./components/SettingsPanel"; @@ -58,7 +60,27 @@ export default function App() { }, 500); }); - return () => { unlisten.then(f => f()); }; + const unlistenFocus = win.listen("tauri://focus", async () => { + const viewer = await WebviewWindow.getByLabel("image-viewer"); + if (viewer) { + const isMin = await viewer.isMinimized(); + if (isMin) await viewer.unminimize(); + } + }); + + const unlistenBlur = win.listen("tauri://blur", async () => { + const isMin = await win.isMinimized(); + if (isMin) { + const viewer = await WebviewWindow.getByLabel("image-viewer"); + if (viewer) await viewer.minimize(); + } + }); + + return () => { + unlisten.then(f => f()); + unlistenFocus.then(f => f()); + unlistenBlur.then(f => f()); + }; }, []); async function handleInitialSync() { @@ -68,6 +90,7 @@ export default function App() { return (
+ setShowSettings(s => !s)} />
diff --git a/src/components/ImageViewerWindow.tsx b/src/components/ImageViewerWindow.tsx new file mode 100644 index 0000000..7afc6ed --- /dev/null +++ b/src/components/ImageViewerWindow.tsx @@ -0,0 +1,74 @@ +import { useState, useEffect } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import ResizeHandles from "./ResizeHandles"; + +export default function ImageViewerWindow() { + const params = new URLSearchParams(window.location.search); + const [imageUrl, setImageUrl] = useState(decodeURIComponent(params.get("imageUrl") ?? "")); + const win = getCurrentWindow(); + + useEffect(() => { + let debounce: ReturnType | null = null; + + const unlistenImage = win.listen("set-viewer-image", (event) => { + setImageUrl(event.payload); + }); + + const unlistenResize = win.onResized(async () => { + if (debounce) clearTimeout(debounce); + debounce = setTimeout(async () => { + const [size, factor] = await Promise.all([win.innerSize(), win.scaleFactor()]); + await invoke("set_setting", { key: "viewer_width", value: String(Math.round(size.width / factor)) }); + await invoke("set_setting", { key: "viewer_height", value: String(Math.round(size.height / factor)) }); + }, 500); + }); + + const unlistenMove = win.onMoved(async () => { + if (debounce) clearTimeout(debounce); + debounce = setTimeout(async () => { + const [pos, factor] = await Promise.all([win.outerPosition(), win.scaleFactor()]); + await invoke("set_setting", { key: "viewer_x", value: String(Math.round(pos.x / factor)) }); + await invoke("set_setting", { key: "viewer_y", value: String(Math.round(pos.y / factor)) }); + }, 500); + }); + + return () => { + unlistenImage.then(f => f()); + unlistenResize.then(f => f()); + unlistenMove.then(f => f()); + }; + }, []); + + return ( +
+ +
{ if (e.button === 0) win.startDragging(); }} + style={{ + height: "28px", + background: "#161b22", + borderBottom: "1px solid #2d3748", + cursor: "grab", + flexShrink: 0, + display: "flex", + alignItems: "center", + paddingLeft: "12px", + userSelect: "none", + }} + > + ⠿ Image +
+ +
+ {imageUrl && ( + + )} +
+
+ ); +} diff --git a/src/components/QuestDetailPanel.tsx b/src/components/QuestDetailPanel.tsx index cc8c487..3293690 100644 --- a/src/components/QuestDetailPanel.tsx +++ b/src/components/QuestDetailPanel.tsx @@ -155,19 +155,21 @@ export default function QuestDetailPanel({ questName, questUrl, profileId, onClo if (done) { const firstLine = step.text.split('\n').find(l => l.trim().length > 0) ?? step.text; return ( -
toggleStep(step.index)} style={{ marginBottom: "4px", padding: "6px 12px", border: "1px solid rgba(74,222,128,0.1)", borderRadius: "7px", background: "rgba(74,222,128,0.03)", opacity: 0.5, + cursor: "pointer", }}>
toggleStep(step.index)} + onClick={e => e.stopPropagation()} style={{ flexShrink: 0, cursor: "pointer" }} /> toggleStep(step.index)} style={{ marginBottom: "8px", background: "rgba(255,255,255,0.02)", border: "1px solid #2d3748", borderRadius: "7px", padding: "10px 12px", transition: "all 0.15s", + cursor: "pointer", }}>
toggleStep(step.index)} + onClick={e => e.stopPropagation()} style={{ marginTop: "2px", flexShrink: 0, cursor: "pointer" }} />
@@ -214,7 +218,7 @@ export default function QuestDetailPanel({ questName, questUrl, profileId, onClo {needsTruncate && (