Add persistent pinned tabs protected from bulk closing
This commit is contained in:
1 parent
19d0feac23
commit
82d08b9d73
4 files changed
+19
-13
No files matched your search
@@ -11,6 +11,7 @@ const paths = {
|
|||||||
chevron: 'm9 6 6 6-6 6',
|
chevron: 'm9 6 6 6-6 6',
|
||||||
more: 'M12 6h.01M12 12h.01M12 18h.01',
|
more: 'M12 6h.01M12 12h.01M12 18h.01',
|
||||||
close: 'M18 6 6 18M6 6l12 12',
|
close: 'M18 6 6 18M6 6l12 12',
|
||||||
|
pin: 'M16 9V3H8v6l-3 3v2h6v7h2v-7h6v-2z',
|
||||||
trash: 'M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6',
|
trash: 'M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6',
|
||||||
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
||||||
restore: 'M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5',
|
restore: 'M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5',
|
||||||
|
|||||||
+12
-10
@@ -3,7 +3,7 @@ import { nextTick, ref } from 'vue'
|
|||||||
import { commandTitle } from './commands'
|
import { commandTitle } from './commands'
|
||||||
import Icon from './Icon.vue'
|
import Icon from './Icon.vue'
|
||||||
import { noteTitle } from './paths'
|
import { noteTitle } from './paths'
|
||||||
import { closeTab, toggleSourceMode, ws } from './workspace'
|
import { closeTab, toggleSourceMode, ws, type Tab } from './workspace'
|
||||||
|
|
||||||
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
|
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
|
||||||
const TAB_TYPE = 'application/x-rawmark-tab'
|
const TAB_TYPE = 'application/x-rawmark-tab'
|
||||||
@@ -42,20 +42,20 @@ function onDragEnd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 右键菜单 ----
|
// ---- 右键菜单 ----
|
||||||
const menu = ref<{ path: string; x: number; y: number } | null>(null)
|
const menu = ref<{ tab: Tab; x: number; y: number } | null>(null)
|
||||||
|
|
||||||
const menuElement = ref<HTMLElement>()
|
const menuElement = ref<HTMLElement>()
|
||||||
async function openMenu(e: MouseEvent, path: string) {
|
async function openMenu(e: MouseEvent, tab: Tab) {
|
||||||
menu.value = { path, x: e.clientX, y: e.clientY }
|
menu.value = { tab, x: e.clientX, y: e.clientY }
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (menu.value?.path !== path || !menuElement.value) return
|
if (menu.value?.tab !== tab || !menuElement.value) return
|
||||||
const { width, height } = menuElement.value.getBoundingClientRect()
|
const { width, height } = menuElement.value.getBoundingClientRect()
|
||||||
menu.value.x = Math.max(4, Math.min(e.clientX, innerWidth - width - 4))
|
menu.value.x = Math.max(4, Math.min(e.clientX, innerWidth - width - 4))
|
||||||
menu.value.y = Math.max(4, Math.min(e.clientY, innerHeight - height - 4))
|
menu.value.y = Math.max(4, Math.min(e.clientY, innerHeight - height - 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeMany(keep: (index: number, path: string) => boolean) {
|
async function closeMany(keep: (index: number, path: string) => boolean) {
|
||||||
const paths = ws.tabs.filter((t, i) => !keep(i, t.path)).map((t) => t.path)
|
const paths = ws.tabs.filter((t, i) => !t.pinned && !keep(i, t.path)).map((t) => t.path)
|
||||||
menu.value = null
|
menu.value = null
|
||||||
for (const p of paths) await closeTab(p)
|
for (const p of paths) await closeTab(p)
|
||||||
}
|
}
|
||||||
@@ -91,12 +91,13 @@ function closeRight(path: string) {
|
|||||||
draggable="true"
|
draggable="true"
|
||||||
@click="ws.active = tab.path"
|
@click="ws.active = tab.path"
|
||||||
@auxclick.prevent="$event.button === 1 && closeTab(tab.path)"
|
@auxclick.prevent="$event.button === 1 && closeTab(tab.path)"
|
||||||
@contextmenu.prevent="openMenu($event, tab.path)"
|
@contextmenu.prevent="openMenu($event, tab)"
|
||||||
@dragstart="onDragStart($event, tab.path)"
|
@dragstart="onDragStart($event, tab.path)"
|
||||||
@dragover="onDragOver($event, tab.path)"
|
@dragover="onDragOver($event, tab.path)"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
>
|
>
|
||||||
|
<span v-if="tab.pinned" class="rm-tab-pin" title="已固定"><Icon name="pin" :size="12" /></span>
|
||||||
<span class="rm-tab-title">{{ noteTitle(tab.path) }}</span>
|
<span class="rm-tab-title">{{ noteTitle(tab.path) }}</span>
|
||||||
<button class="rm-tab-close" :class="{ dirty: tab.dirty }" title="关闭" @click.stop="closeTab(tab.path)">
|
<button class="rm-tab-close" :class="{ dirty: tab.dirty }" title="关闭" @click.stop="closeTab(tab.path)">
|
||||||
<span class="rm-dirty-dot" />
|
<span class="rm-dirty-dot" />
|
||||||
@@ -114,9 +115,10 @@ function closeRight(path: string) {
|
|||||||
<template v-if="menu">
|
<template v-if="menu">
|
||||||
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
||||||
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
|
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
|
||||||
<button @click="closeMany((_, p) => p !== menu!.path)">关闭</button>
|
<button @click="menu.tab.pinned = !menu.tab.pinned; menu = null">{{ menu.tab.pinned ? '取消固定' : '固定' }}</button>
|
||||||
<button @click="closeOthers(menu.path)">关闭其他标签页</button>
|
<button @click="closeTab(menu.tab.path); menu = null">关闭</button>
|
||||||
<button @click="closeRight(menu.path)">关闭右侧标签页</button>
|
<button @click="closeOthers(menu.tab.path)">关闭其他标签页</button>
|
||||||
|
<button @click="closeRight(menu.tab.path)">关闭右侧标签页</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ button {
|
|||||||
.rm-tab:hover { background: var(--rm-hover); }
|
.rm-tab:hover { background: var(--rm-hover); }
|
||||||
.rm-tab.active { background: var(--rm-active); color: var(--rm-text); }
|
.rm-tab.active { background: var(--rm-active); color: var(--rm-text); }
|
||||||
.rm-tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.rm-tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.rm-tab-pin { display: inline-flex; flex: none; color: var(--rm-accent-strong); }
|
||||||
.rm-tab-close {
|
.rm-tab-close {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type TabStatus = 'loading' | 'ready' | 'conflict' | 'deleted' | 'error'
|
|||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
path: string
|
path: string
|
||||||
|
pinned?: boolean
|
||||||
// 最近一次与服务器一致的版本
|
// 最近一次与服务器一致的版本
|
||||||
rev: string
|
rev: string
|
||||||
status: TabStatus
|
status: TabStatus
|
||||||
@@ -957,7 +958,7 @@ export async function startWorkspace() {
|
|||||||
const settingsLoaded = loadSettings().catch((err) => toast('读取设置失败:' + errorText(err)))
|
const settingsLoaded = loadSettings().catch((err) => toast('读取设置失败:' + errorText(err)))
|
||||||
await refreshTree()
|
await refreshTree()
|
||||||
await settingsLoaded
|
await settingsLoaded
|
||||||
const saved = JSON.parse(localStorage.getItem(TABS_KEY) ?? '{"paths":[],"active":null}') as { paths: string[]; active: string | null }
|
const saved = JSON.parse(localStorage.getItem(TABS_KEY) ?? '{"paths":[],"active":null}') as { paths: string[]; active: string | null; pinned?: string[] }
|
||||||
const exists = new Set(ws.entries.map((e) => e.path))
|
const exists = new Set(ws.entries.map((e) => e.path))
|
||||||
await Promise.all(saved.paths.filter((p) => exists.has(p)).map(openNote))
|
await Promise.all(saved.paths.filter((p) => exists.has(p)).map(openNote))
|
||||||
try {
|
try {
|
||||||
@@ -965,6 +966,7 @@ export async function startWorkspace() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast('恢复本地草稿失败:' + errorText(err))
|
toast('恢复本地草稿失败:' + errorText(err))
|
||||||
}
|
}
|
||||||
|
for (const tab of ws.tabs) tab.pinned = saved.pinned?.includes(tab.path) ?? false
|
||||||
ws.active = saved.active && tabOf(saved.active) ? saved.active : (ws.tabs[0]?.path ?? null)
|
ws.active = saved.active && tabOf(saved.active) ? saved.active : (ws.tabs[0]?.path ?? null)
|
||||||
let eventQueue = Promise.resolve()
|
let eventQueue = Promise.resolve()
|
||||||
let stopped = false
|
let stopped = false
|
||||||
@@ -991,8 +993,8 @@ export async function startWorkspace() {
|
|||||||
if (listenersInstalled) return
|
if (listenersInstalled) return
|
||||||
listenersInstalled = true
|
listenersInstalled = true
|
||||||
watch(
|
watch(
|
||||||
() => [ws.tabs.map((t) => t.path), ws.active] as const,
|
() => ({ paths: ws.tabs.map((t) => t.path), active: ws.active, pinned: ws.tabs.filter((t) => t.pinned).map((t) => t.path) }),
|
||||||
([paths, active]) => localStorage.setItem(TABS_KEY, JSON.stringify({ paths, active })),
|
(tabs) => localStorage.setItem(TABS_KEY, JSON.stringify(tabs)),
|
||||||
// 启动期间(如等待目录刷新时)打开的标签页也要立即记录
|
// 启动期间(如等待目录刷新时)打开的标签页也要立即记录
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user