自定义装饰栏功能
This commit is contained in:
@@ -5,6 +5,10 @@
|
|||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"dialog:default"
|
"dialog:default"
|
||||||
]
|
]
|
||||||
|
|||||||
241
src/App.vue
241
src/App.vue
@@ -2,6 +2,7 @@
|
|||||||
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
||||||
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||||
import {invoke, isTauri} from "@tauri-apps/api/core";
|
import {invoke, isTauri} from "@tauri-apps/api/core";
|
||||||
|
import {getCurrentWindow} from "@tauri-apps/api/window";
|
||||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||||
@@ -135,10 +136,105 @@ const showMoreMenu = ref(false);
|
|||||||
const settingsRequest = ref(0);
|
const settingsRequest = ref(0);
|
||||||
const settingsInitialPage = ref<"about" | "editor">("about");
|
const settingsInitialPage = ref<"about" | "editor">("about");
|
||||||
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
||||||
|
const isWindowMaximized = ref(false);
|
||||||
const showStartActions = computed(() => {
|
const showStartActions = computed(() => {
|
||||||
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type TitlebarDragState = {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TITLEBAR_DRAG_THRESHOLD = 4;
|
||||||
|
let titlebarDragState: TitlebarDragState | null = null;
|
||||||
|
let unlistenWindowResize: (() => void) | null = null;
|
||||||
|
|
||||||
|
function isInteractiveTitlebarTarget(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof Element)) return false;
|
||||||
|
return Boolean(target.closest("button, a, input, textarea, select, [data-no-window-drag]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshWindowMaximized() {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
try {
|
||||||
|
isWindowMaximized.value = await getCurrentWindow().isMaximized();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to read window state:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function minimizeWindow() {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().minimize();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to minimize window:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleWindowMaximize() {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
try {
|
||||||
|
const appWindow = getCurrentWindow();
|
||||||
|
await appWindow.toggleMaximize();
|
||||||
|
isWindowMaximized.value = await appWindow.isMaximized();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to toggle window maximize:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeWindow() {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to close window:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTitlebarDragListeners() {
|
||||||
|
window.removeEventListener("mousemove", onTitlebarMouseMove);
|
||||||
|
window.removeEventListener("mouseup", removeTitlebarDragListeners);
|
||||||
|
titlebarDragState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTitlebarMouseDown(event: MouseEvent) {
|
||||||
|
if (event.button !== 0 || event.detail > 1 || isInteractiveTitlebarTarget(event.target)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
titlebarDragState = {
|
||||||
|
startX: event.screenX,
|
||||||
|
startY: event.screenY,
|
||||||
|
};
|
||||||
|
window.addEventListener("mousemove", onTitlebarMouseMove);
|
||||||
|
window.addEventListener("mouseup", removeTitlebarDragListeners, {once: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTitlebarMouseMove(event: MouseEvent) {
|
||||||
|
if (!titlebarDragState) return;
|
||||||
|
|
||||||
|
const movedX = Math.abs(event.screenX - titlebarDragState.startX);
|
||||||
|
const movedY = Math.abs(event.screenY - titlebarDragState.startY);
|
||||||
|
if (movedX < TITLEBAR_DRAG_THRESHOLD && movedY < TITLEBAR_DRAG_THRESHOLD) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
removeTitlebarDragListeners();
|
||||||
|
if (!isTauri()) return;
|
||||||
|
|
||||||
|
void getCurrentWindow().startDragging().catch((error) => {
|
||||||
|
console.error("Failed to start window drag:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTitlebarDoubleClick(event: MouseEvent) {
|
||||||
|
if (isInteractiveTitlebarTarget(event.target)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
removeTitlebarDragListeners();
|
||||||
|
void toggleWindowMaximize();
|
||||||
|
}
|
||||||
|
|
||||||
function openSettings(page: "about" | "editor" = "about") {
|
function openSettings(page: "about" | "editor" = "about") {
|
||||||
settingsInitialPage.value = page;
|
settingsInitialPage.value = page;
|
||||||
settingsRequest.value += 1;
|
settingsRequest.value += 1;
|
||||||
@@ -181,11 +277,26 @@ onMounted(() => {
|
|||||||
applyAppTheme(appTheme.value);
|
applyAppTheme(appTheme.value);
|
||||||
applyZoomCssVars();
|
applyZoomCssVars();
|
||||||
window.addEventListener("wheel", onEditorWheel, {passive: false});
|
window.addEventListener("wheel", onEditorWheel, {passive: false});
|
||||||
|
if (isTauri()) {
|
||||||
|
void refreshWindowMaximized();
|
||||||
|
void getCurrentWindow().onResized(() => {
|
||||||
|
void refreshWindowMaximized();
|
||||||
|
}).then((unlisten) => {
|
||||||
|
unlistenWindowResize = unlisten;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to listen for window resize:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
void restoreLastWorkspace();
|
void restoreLastWorkspace();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener("wheel", onEditorWheel);
|
window.removeEventListener("wheel", onEditorWheel);
|
||||||
|
removeTitlebarDragListeners();
|
||||||
|
if (unlistenWindowResize) {
|
||||||
|
unlistenWindowResize();
|
||||||
|
unlistenWindowResize = null;
|
||||||
|
}
|
||||||
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -478,28 +589,45 @@ registerTauriListener("menu-event", (event) => {
|
|||||||
|
|
||||||
<!-- Right: Editor area -->
|
<!-- Right: Editor area -->
|
||||||
<main class="flex-1 flex flex-col overflow-hidden relative">
|
<main class="flex-1 flex flex-col overflow-hidden relative">
|
||||||
<!-- ════════════════ Toolbar ════════════════ -->
|
<!-- ════════════════ Custom title bar / toolbar ════════════════ -->
|
||||||
<div
|
<div
|
||||||
v-if="!showStartActions"
|
class="editor-titlebar flex w-full items-center h-10 pl-3 pr-0 select-none shrink-0"
|
||||||
class="editor-toolbar flex w-full items-center h-10 px-3 select-none shrink-0"
|
@mousedown="onTitlebarMouseDown"
|
||||||
|
@dblclick="onTitlebarDoubleClick"
|
||||||
>
|
>
|
||||||
<!-- Left: Outline toggle -->
|
<!-- Left: Outline toggle -->
|
||||||
<button
|
<div
|
||||||
class="toolbar-btn"
|
v-if="!showStartActions"
|
||||||
:class="{ 'toolbar-btn-active': showOutline }"
|
class="flex items-center shrink-0"
|
||||||
title="切换大纲"
|
data-no-window-drag
|
||||||
@click="showOutline = !showOutline"
|
|
||||||
>
|
>
|
||||||
<i class="ri-menu-line text-base"></i>
|
<button
|
||||||
</button>
|
class="toolbar-btn"
|
||||||
|
:class="{ 'toolbar-btn-active': showOutline }"
|
||||||
|
title="切换大纲"
|
||||||
|
aria-label="切换大纲"
|
||||||
|
@click="showOutline = !showOutline"
|
||||||
|
>
|
||||||
|
<i class="ri-menu-line text-base"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="titlebar-brand app-muted-text shrink-0 pointer-events-none">
|
||||||
|
yurou
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Center: Filename -->
|
<!-- Center: Filename -->
|
||||||
<div class="app-muted-text flex-1 text-center text-sm font-medium truncate px-3 select-none">
|
<div class="titlebar-drag-surface flex-1 min-w-0 px-3">
|
||||||
|
<div class="app-muted-text titlebar-file-title text-center text-sm font-medium truncate pointer-events-none">
|
||||||
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
|
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Actions -->
|
<!-- Right: Actions -->
|
||||||
<div v-if="!showStartActions" class="flex items-center gap-0.5">
|
<div
|
||||||
|
v-if="!showStartActions"
|
||||||
|
class="flex items-center gap-0.5 shrink-0"
|
||||||
|
data-no-window-drag
|
||||||
|
>
|
||||||
<!-- Document mode -->
|
<!-- Document mode -->
|
||||||
<div class="mode-toggle" role="group" aria-label="文档模式">
|
<div class="mode-toggle" role="group" aria-label="文档模式">
|
||||||
<button
|
<button
|
||||||
@@ -557,6 +685,34 @@ registerTauriListener("menu-event", (event) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Window controls -->
|
||||||
|
<div class="window-controls flex items-stretch self-stretch ml-2 shrink-0" data-no-window-drag>
|
||||||
|
<button
|
||||||
|
class="window-control-btn"
|
||||||
|
title="最小化"
|
||||||
|
aria-label="最小化"
|
||||||
|
@click="minimizeWindow"
|
||||||
|
>
|
||||||
|
<i class="ri-subtract-line"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="window-control-btn"
|
||||||
|
:title="isWindowMaximized ? '还原' : '最大化'"
|
||||||
|
:aria-label="isWindowMaximized ? '还原' : '最大化'"
|
||||||
|
@click="toggleWindowMaximize"
|
||||||
|
>
|
||||||
|
<i :class="isWindowMaximized ? 'ri-checkbox-multiple-blank-line' : 'ri-checkbox-blank-line'"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="window-control-btn window-control-close"
|
||||||
|
title="关闭"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click="closeWindow"
|
||||||
|
>
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex min-h-0 overflow-hidden">
|
<div class="flex-1 flex min-h-0 overflow-hidden">
|
||||||
<Transition name="outline-slide">
|
<Transition name="outline-slide">
|
||||||
@@ -657,7 +813,7 @@ registerTauriListener("menu-event", (event) => {
|
|||||||
color: var(--app-text);
|
color: var(--app-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar {
|
.editor-titlebar {
|
||||||
background: var(--app-bg);
|
background: var(--app-bg);
|
||||||
border-bottom: 1px solid var(--app-border-strong);
|
border-bottom: 1px solid var(--app-border-strong);
|
||||||
}
|
}
|
||||||
@@ -686,6 +842,28 @@ registerTauriListener("menu-event", (event) => {
|
|||||||
|
|
||||||
/* ── Toolbar ───────────────────────────────────────────────────── */
|
/* ── Toolbar ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.titlebar-drag-surface {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar-brand {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 68px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar-file-title {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-btn {
|
.toolbar-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -711,6 +889,43 @@ registerTauriListener("menu-event", (event) => {
|
|||||||
background-color: var(--app-surface-muted);
|
background-color: var(--app-surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-btn {
|
||||||
|
width: 46px;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-btn:hover {
|
||||||
|
background-color: var(--app-hover);
|
||||||
|
color: var(--app-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-btn:focus-visible {
|
||||||
|
outline: 1px solid var(--app-accent);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-close:hover {
|
||||||
|
background-color: #c42b1c;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.mode-toggle {
|
.mode-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user