Add settings page with synced appearance and password change
- Store app settings as one JSON document on the server, merged per key and pushed to other devices; cache locally to avoid a flash on load - Settings page groups appearance, account, shortcuts, shares, API tokens and trash; full screen with a section list on phones - Font size and content width settings - Change the login password, signing out other sessions - Settings writes and password changes only accept a login session
This commit is contained in:
1 parent
5b326a70bf
commit
eb63b14c8e
25 files changed
+915
-290
No files matched your search
@@ -9,6 +9,7 @@ import { api, ApiError, type Entry } from './api'
|
||||
import { drafts } from './drafts'
|
||||
import { connectEvents, type VaultEvent } from './events'
|
||||
import { basename, dirname, joinPath, NOTE_EXT, resolveAssetUrl } from './paths'
|
||||
import { loadSettings, settings } from './settings'
|
||||
|
||||
export type TabStatus = 'loading' | 'ready' | 'conflict' | 'deleted' | 'error'
|
||||
|
||||
@@ -24,6 +25,8 @@ export interface Tab {
|
||||
conflictRev?: string
|
||||
}
|
||||
|
||||
export type SettingsSection = 'appearance' | 'account' | 'shortcuts' | 'shares' | 'tokens' | 'trash'
|
||||
|
||||
export interface Toast {
|
||||
id: number
|
||||
text: string
|
||||
@@ -45,10 +48,10 @@ export const ws = reactive({
|
||||
searchOpen: false,
|
||||
// 快速切换/命令面板的初始输入,null 为关闭;以 > 开头时列出命令
|
||||
switcher: null as string | null,
|
||||
trashOpen: false,
|
||||
// 分享对话框:path 为笔记路径,null 为全部分享的管理列表
|
||||
shareDialog: null as { path: string | null } | null,
|
||||
tokensOpen: false,
|
||||
// 打开的设置分组,null 为关闭;index 为从通用入口打开(手机上先显示分组列表,电脑上显示第一组)
|
||||
settings: null as SettingsSection | 'index' | null,
|
||||
// 分享当前笔记的对话框(笔记路径)
|
||||
sharing: null as string | null,
|
||||
// 最近打开的笔记,最新的在前
|
||||
recent: JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[],
|
||||
toasts: [] as Toast[],
|
||||
@@ -646,6 +649,7 @@ async function syncOpenTab(tab: Tab) {
|
||||
function handleEvents(evs: VaultEvent[]) {
|
||||
for (const ev of evs) {
|
||||
if (ev.type === 'tree') void refreshTree()
|
||||
else if (ev.type === 'settings') void loadSettings().catch((err) => toast('同步设置失败:' + errorText(err)))
|
||||
else if (ev.type === 'changed') {
|
||||
const tab = tabOf(ev.path!)
|
||||
// 自己保存产生的事件 rev 与本地一致;有本地修改时不覆盖,交由下次保存的冲突检测处理
|
||||
@@ -701,7 +705,10 @@ let disconnect: (() => void) | undefined
|
||||
let listenersInstalled = false
|
||||
|
||||
export async function startWorkspace() {
|
||||
// 设置读取失败不影响使用,沿用本地缓存的值
|
||||
const settingsLoaded = loadSettings().catch((err) => toast('读取设置失败:' + errorText(err)))
|
||||
await refreshTree()
|
||||
await settingsLoaded
|
||||
const saved = JSON.parse(localStorage.getItem(TABS_KEY) ?? '{"paths":[],"active":null}') as { paths: string[]; active: string | null }
|
||||
const exists = new Set(ws.entries.map((e) => e.path))
|
||||
await Promise.all(saved.paths.filter((p) => exists.has(p)).map(openNote))
|
||||
@@ -732,6 +739,11 @@ export async function startWorkspace() {
|
||||
() => ws.recent,
|
||||
(r) => localStorage.setItem(RECENT_KEY, JSON.stringify(r)),
|
||||
)
|
||||
// 字号、正文宽度变化后行高随之改变,编辑器需要重新测量
|
||||
watch(
|
||||
() => [settings.fontSize, settings.contentWidth],
|
||||
() => view?.requestMeasure(),
|
||||
)
|
||||
// 页面隐藏/关闭前尽力发出待保存的修改(部分浏览器关闭标签页时只触发 pagehide)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') void flushAll(true)
|
||||
@@ -760,10 +772,11 @@ function registerBuiltinCommands() {
|
||||
{ id: 'note.delete', title: '删除当前笔记', when: hasNote, run: () => void remove(ws.active!) },
|
||||
// 浏览器不允许网页覆盖 Ctrl+W
|
||||
{ id: 'tab.close', title: '关闭当前标签页', when: hasNote, run: () => void closeTab(ws.active!) },
|
||||
{ id: 'app.trash', title: '打开回收站', run: () => (ws.trashOpen = true) },
|
||||
{ id: 'note.share', title: '分享当前笔记', when: hasNote, run: () => (ws.shareDialog = { path: ws.active }) },
|
||||
{ id: 'app.shares', title: '管理分享链接', run: () => (ws.shareDialog = { path: null }) },
|
||||
{ id: 'app.tokens', title: 'API 令牌', run: () => (ws.tokensOpen = true) },
|
||||
{ id: 'app.settings', title: '设置', keys: ['Mod+,'], run: () => (ws.settings = 'index') },
|
||||
{ id: 'app.trash', title: '打开回收站', run: () => (ws.settings = 'trash') },
|
||||
{ id: 'note.share', title: '分享当前笔记', when: hasNote, run: () => (ws.sharing = ws.active) },
|
||||
{ id: 'app.shares', title: '管理分享链接', run: () => (ws.settings = 'shares') },
|
||||
{ id: 'app.tokens', title: 'API 令牌', run: () => (ws.settings = 'tokens') },
|
||||
]) {
|
||||
registerCommand(cmd)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user