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
164 changes: 157 additions & 7 deletions src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import useFont from "@/renderer/hooks/useFont";
import useOtherConfig from "@/renderer/hooks/useOtherConfig";
import { isShowOutline, toggleShowOutline } from "@/renderer/hooks/useOutline";
import { useSaveConfirmDialog } from "@/renderer/hooks/useSaveConfirmDialog";
import {
createSidebarWidthController,
SIDEBAR_DEFAULT_CSS,
} from "@/renderer/hooks/useSidebarWidth";
import useSourceCode from "@/renderer/hooks/useSourceCode";
import useSpellCheck from "@/renderer/hooks/useSpellCheck";
import useTab from "@/renderer/hooks/useTab";
Expand All @@ -14,6 +18,12 @@ import useUiLoading from "@/renderer/hooks/useUiLoading";
import { useUpdateDialog } from "@/renderer/hooks/useUpdateDialog";
import useWorkSpace from "@/renderer/hooks/useWorkSpace";
import { shouldAutoLoadWorkspace } from "@/renderer/utils/workspacePath";
import {
getSidebarContextKey,
readSidebarVisibility,
resolveSidebarVisibility,
writeSidebarVisibility,
} from "@/renderer/utils/sidebarVisibility";
import SaveConfirmDialog from "./components/dialogs/SaveConfirmDialog.vue";
import UpdateConfirmDialog from "./components/dialogs/UpdateConfirmDialog.vue";
import MilkupEditor from "./components/editor/MilkupEditor.vue";
Expand All @@ -30,8 +40,8 @@ const { init: initTheme } = useTheme();
const { isLoading, loadingMessage } = useUiLoading();
const { init: initFont } = useFont();
const { init: initOtherConfig } = useOtherConfig();
const { config } = useConfig();
const { openWorkSpaceByPath } = useWorkSpace();
const { config, setConf } = useConfig();
const { openWorkSpaceByPath, watchedDirPath } = useWorkSpace();
const { isShowSource } = useSourceCode(); // 用于控制大纲显示
const { init: initSpellCheck } = useSpellCheck();
const {
Expand Down Expand Up @@ -118,16 +128,119 @@ window.electronAPI.on("update:available", onUpdateAvailable);

// 监听手动检查的更新可用事件 (Manual Check from About)
import { onMounted, onUnmounted, ref, watch, nextTick, computed } from "vue";

// 大纲侧边栏两阶段动画状态机
// closed: 隐藏 | opening: transform 滑入动画 | open: flex 正常布局 | closing-prep: 切回 transform 定位 | closing: transform 滑出动画
type OutlineState = "closed" | "opening" | "open" | "closing-prep" | "closing";
const initialOutlineVisible = Boolean(config.value.workspace?.autoExpandSidebar);
function sidebarVisibilityForContext(contextKey: string): boolean {
return resolveSidebarVisibility(
readSidebarVisibility(contextKey),
Boolean(config.value.workspace?.autoExpandSidebar)
);
}

const initialSidebarContextKey = getSidebarContextKey(
watchedDirPath.value ?? config.value.workspace?.startupPath
);
let activeSidebarContextKey = initialSidebarContextKey;
let applyingSidebarContext = false;
const initialOutlineVisible = sidebarVisibilityForContext(initialSidebarContextKey);
toggleShowOutline(initialOutlineVisible);
const outlineState = ref<OutlineState>(initialOutlineVisible ? "open" : "closed");
const editorAreaRef = ref<HTMLElement | null>(null);

const sidebarWidthController = createSidebarWidthController({
getContainerWidth: () => editorAreaRef.value?.getBoundingClientRect().width ?? 0,
getPreferredWidth: () => config.value.workspace?.sidebarWidth,
setPreferredWidth: (width) => {
setConf("workspace", {
...config.value.workspace,
sidebarWidth: width,
});
},
});
const savedSidebarWidth = config.value.workspace?.sidebarWidth;
const sidebarWidth = ref<number | null>(
typeof savedSidebarWidth === "number" && Number.isFinite(savedSidebarWidth)
? savedSidebarWidth
: null
);
const sidebarResizing = ref(false);
const editorAreaStyle = computed(() => ({
"--sidebar-width": sidebarWidth.value === null ? SIDEBAR_DEFAULT_CSS : `${sidebarWidth.value}px`,
}));

function syncSidebarWidth() {
if (!sidebarResizing.value) {
sidebarWidth.value = sidebarWidthController.refresh();
}
}

function addSidebarResizeListeners() {
window.addEventListener("pointermove", moveSidebarResize);
window.addEventListener("pointerup", finishSidebarResize);
window.addEventListener("pointercancel", finishSidebarResize);
window.addEventListener("blur", finishSidebarResize);
}

function removeSidebarResizeListeners() {
window.removeEventListener("pointermove", moveSidebarResize);
window.removeEventListener("pointerup", finishSidebarResize);
window.removeEventListener("pointercancel", finishSidebarResize);
window.removeEventListener("blur", finishSidebarResize);
}

function startSidebarResize(event: PointerEvent) {
if (event.button !== 0) return;
event.preventDefault();
const handle = event.currentTarget as HTMLElement | null;
try {
handle?.setPointerCapture(event.pointerId);
} catch {
// Window-level listeners remain the fallback when pointer capture is unavailable.
}
sidebarWidthController.beginResize(event.clientX);
sidebarResizing.value = true;
sidebarWidth.value = sidebarWidthController.getWidth();
addSidebarResizeListeners();
}

function moveSidebarResize(event: PointerEvent) {
if (!sidebarResizing.value) return;
event.preventDefault();
sidebarWidth.value = sidebarWidthController.resizeTo(event.clientX);
}

function finishSidebarResize() {
if (!sidebarResizing.value) return;
sidebarResizing.value = false;
sidebarWidthController.endResize();
sidebarWidth.value = sidebarWidthController.getWidth();
removeSidebarResizeListeners();
}

const outlineClass = computed(() => `outline-${outlineState.value}`);

watch(
isShowOutline,
(visible) => {
if (!applyingSidebarContext) {
writeSidebarVisibility(activeSidebarContextKey, visible);
}
},
{ flush: "sync" }
);

watch(watchedDirPath, (workspacePath) => {
const nextSidebarContextKey = getSidebarContextKey(workspacePath);
if (nextSidebarContextKey === activeSidebarContextKey) return;

activeSidebarContextKey = nextSidebarContextKey;
applyingSidebarContext = true;
toggleShowOutline(sidebarVisibilityForContext(nextSidebarContextKey));
applyingSidebarContext = false;
});

watch(isShowOutline, async (val) => {
if (val) {
outlineState.value = "opening";
Expand All @@ -150,6 +263,7 @@ function onOutlineTransitionEnd(e: TransitionEvent) {
}

onMounted(() => {
void nextTick().then(syncSidebarWidth);
initTheme();
initFont();
initOtherConfig();
Expand All @@ -164,9 +278,15 @@ onMounted(() => {
}
emitter.on("update:available", onUpdateAvailable);
});
watch(
() => config.value.workspace?.sidebarWidth,
() => syncSidebarWidth()
);

onUnmounted(() => {
emitter.off("update:available", onUpdateAvailable);
emitter.off("tab:close-confirm", handleTabCloseConfirm);
removeSidebarResizeListeners();
});

// Reuse safe close logic
Expand Down Expand Up @@ -222,9 +342,10 @@ const handleInstall = async () => {
<TitleBar />
<div id="fontRoot">
<!-- ✅ 多编辑器实例:每个 tab 拥有独立的编辑器,v-show 保持 DOM 存活 -->
<div ref="editorAreaRef" class="editorArea" :class="outlineClass">
<div ref="editorAreaRef" class="editorArea" :class="outlineClass" :style="editorAreaStyle">
<div class="outlineBox">
<Outline />
<div class="sidebar-resize-handle" @pointerdown="startSidebarResize" />
</div>
<div class="editorBox" @transitionend="onOutlineTransitionEnd">
<!-- Milkup 编辑器(每个 tab 独立实例) -->
Expand Down Expand Up @@ -295,7 +416,7 @@ const handleInstall = async () => {
position: absolute;
left: 0;
top: 0;
width: 25%;
width: var(--sidebar-width);
height: 100%;
z-index: 10;
transform: translateX(-100%);
Expand All @@ -306,9 +427,38 @@ const handleInstall = async () => {
opacity 0.2s ease;
}

.sidebar-resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 20;
width: 10px;
cursor: ew-resize;
touch-action: none;
background: transparent;

&::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
right: -1px;
width: 2px;
background: var(--primary-color);
opacity: 0.35;
transition: opacity 0.15s ease;
}

&:hover::after {
opacity: 1;
}
}

.editorBox {
flex: 1;
width: 100%;
cursor: default;
transition: transform 0.2s ease;
}

Expand All @@ -320,7 +470,7 @@ const handleInstall = async () => {
pointer-events: auto;
}
.editorBox {
transform: translateX(25%);
transform: translateX(var(--sidebar-width));
}
}

Expand Down Expand Up @@ -352,7 +502,7 @@ const handleInstall = async () => {
}
.editorBox {
width: 100%;
transform: translateX(25%);
transform: translateX(var(--sidebar-width));
transition: none;
}
}
Expand Down
25 changes: 14 additions & 11 deletions src/renderer/components/settings/WorkspaceSetting.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { computed, onMounted, ref, watch } from "vue";
import AppIcon from "@/renderer/components/ui/AppIcon.vue";
import { Switch } from "@renderer/components/ui/switch";
import { useConfig } from "@/renderer/hooks/useConfig";
import { isShowOutline, toggleShowOutline } from "@/renderer/hooks/useOutline";
import useWorkSpace from "@/renderer/hooks/useWorkSpace";
import { isAbsoluteLocalPath } from "@/renderer/utils/workspacePath";

const { config, setConf } = useConfig();
const { watchedDirPath, openWorkSpaceByPath } = useWorkSpace();

const startupPath = computed(() => config.value.workspace?.startupPath ?? "");
const autoExpandSidebar = computed(() => config.value.workspace?.autoExpandSidebar ?? false);
const isPathExists = ref(true);

async function handleSelectWorkspace() {
Expand Down Expand Up @@ -43,13 +43,6 @@ function clearWorkspacePath() {
});
}

function updateAutoExpandSidebar(value: boolean) {
setConf("workspace", {
...config.value.workspace,
autoExpandSidebar: value,
});
}

async function checkWorkspacePath() {
if (!startupPath.value) {
isPathExists.value = true;
Expand Down Expand Up @@ -103,10 +96,13 @@ onMounted(() => {
<span class="row-label">左侧边栏</span>
<div class="switch-wrapper">
<Switch
:model-value="autoExpandSidebar"
label="启动时自动展开文件夹 / 大纲栏"
@update:model-value="updateAutoExpandSidebar"
:model-value="isShowOutline"
label="显示侧边栏"
@update:model-value="toggleShowOutline"
/>
<p class="setting-hint">
关闭或打开侧边栏后会立即生效;关闭程序后,下次打开时仍保持最后的选择。
</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -201,6 +197,13 @@ onMounted(() => {

.switch-wrapper {
padding-top: 8px;

.setting-hint {
margin: 6px 0 0;
color: var(--text-color-3);
font-size: 12px;
line-height: 1.5;
}
}

.path-tip {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/hooks/useConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface AppConfig extends Record<string, any> {
sortBy: "name" | "mtime";
startupPath: string;
autoExpandSidebar: boolean;
sidebarWidth: number | null;
};
}

Expand All @@ -57,6 +58,7 @@ const defaultConfig: AppConfig = {
sortBy: "name",
startupPath: "",
autoExpandSidebar: false,
sidebarWidth: null,
},
};

Expand Down
Loading
Loading