design: upgrade global design of the app

This commit is contained in:
2026-04-23 11:21:38 +02:00
parent a397c86bc3
commit fe108a3b61
9 changed files with 208 additions and 10 deletions

View File

@ -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 (
<div className="app-shell">
<ResizeHandles />
<TitleBar onOpenSettings={() => setShowSettings(s => !s)} />
<div className="app-body">
<main className="app-main">

View File

@ -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<typeof setTimeout> | null = null;
const unlistenImage = win.listen<string>("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 (
<div style={{ height: "100vh", display: "flex", flexDirection: "column", background: "#0d1117", overflow: "hidden" }}>
<ResizeHandles />
<div
onMouseDown={e => { 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",
}}
>
<span style={{ fontSize: "11px", color: "#4a5568", pointerEvents: "none" }}> Image</span>
</div>
<div style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}>
{imageUrl && (
<img
src={imageUrl}
style={{ width: "100%", display: "block" }}
draggable={false}
/>
)}
</div>
</div>
);
}

View File

@ -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 (
<div key={step.index} style={{
<div key={step.index} onClick={() => 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",
}}>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
<input
type="checkbox"
checked={true}
onChange={() => toggleStep(step.index)}
onClick={e => e.stopPropagation()}
style={{ flexShrink: 0, cursor: "pointer" }}
/>
<span style={{
@ -190,18 +192,20 @@ export default function QuestDetailPanel({ questName, questUrl, profileId, onClo
: step.text;
return (
<div key={step.index} style={{
<div key={step.index} onClick={() => 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",
}}>
<div style={{ display: "flex", gap: "10px", alignItems: "flex-start" }}>
<input
type="checkbox"
checked={false}
onChange={() => toggleStep(step.index)}
onClick={e => e.stopPropagation()}
style={{ marginTop: "2px", flexShrink: 0, cursor: "pointer" }}
/>
<div style={{ flex: 1, minWidth: 0 }}>
@ -214,7 +218,7 @@ export default function QuestDetailPanel({ questName, questUrl, profileId, onClo
{needsTruncate && (
<button
onClick={() => toggleExpanded(step.index)}
onClick={e => { e.stopPropagation(); toggleExpanded(step.index); }}
style={{
marginTop: "4px", background: "transparent", border: "none",
color: "#4a9eff", fontSize: "11px", cursor: "pointer",
@ -231,7 +235,7 @@ export default function QuestDetailPanel({ questName, questUrl, profileId, onClo
<img
key={j}
src={src}
onClick={() => openUrl(src)}
onClick={e => { e.stopPropagation(); invoke("open_image_viewer", { imageUrl: src }); }}
style={{
maxWidth: "100%", height: "auto",
borderRadius: "6px", display: "block",
@ -279,7 +283,7 @@ function QuestHeader({ step }: { step: QuestStep }) {
<img
key={j}
src={src}
onClick={() => openUrl(src)}
onClick={() => invoke("open_image_viewer", { imageUrl: src })}
style={{
maxWidth: "100%", height: "auto",
borderRadius: "6px", display: "block",

View File

@ -0,0 +1,31 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { ResizeEdge } from "@tauri-apps/api/window";
const S = 8; // grab zone size in px
const handles: { edge: ResizeEdge; style: React.CSSProperties }[] = [
{ edge: "North", style: { top: 0, left: S, right: S, height: S, cursor: "n-resize" } },
{ edge: "South", style: { bottom: 0, left: S, right: S, height: S, cursor: "s-resize" } },
{ edge: "West", style: { top: S, left: 0, bottom: S, width: S, cursor: "w-resize" } },
{ edge: "East", style: { top: S, right: 0, bottom: S, width: S, cursor: "e-resize" } },
{ edge: "NorthWest", style: { top: 0, left: 0, width: S, height: S, cursor: "nw-resize" } },
{ edge: "NorthEast", style: { top: 0, right: 0, width: S, height: S, cursor: "ne-resize" } },
{ edge: "SouthWest", style: { bottom: 0, left: 0, width: S, height: S, cursor: "sw-resize" } },
{ edge: "SouthEast", style: { bottom: 0, right: 0,width: S, height: S, cursor: "se-resize" } },
];
export default function ResizeHandles() {
const win = getCurrentWindow();
return (
<>
{handles.map(({ edge, style }) => (
<div
key={edge}
onMouseDown={e => { if (e.button === 0) win.startResizeDragging(edge); }}
style={{ position: "fixed", zIndex: 9999, ...style }}
/>
))}
</>
);
}

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useStore } from "../store";
function useWindowWidth() {
@ -27,6 +28,10 @@ export default function TitleBar({ onOpenSettings }: Props) {
}
async function handleClose() {
const viewer = await WebviewWindow.getByLabel("image-viewer");
if (viewer) {
try { await viewer.close(); } catch (_) {}
}
await getCurrentWindow().close();
}

View File

@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import ImageViewerWindow from "./components/ImageViewerWindow";
// Block all document-level scrolling
document.addEventListener("wheel", (e) => {
@ -17,8 +18,10 @@ document.addEventListener("scroll", () => {
window.scrollTo(0, 0);
}, { passive: true });
const isViewer = new URLSearchParams(window.location.search).get("viewer") === "1";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
{isViewer ? <ImageViewerWindow /> : <App />}
</React.StrictMode>,
);