From d6245a74d3afd7fd14e2564f5a24fca60eca1ea6 Mon Sep 17 00:00:00 2001 From: huojl Date: Sat, 13 Jun 2026 12:47:27 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=92=8C=E6=A0=B7=E5=BC=8F=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2=E5=92=8C=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=A8=A1=E6=80=81=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.ts | 228 +++++++++++----------- src/main.ts | 254 ++++++++++++------------ src/settings.ts | 502 ++++++++++++++++++++++++------------------------ src/types.ts | 176 ++++++++--------- styles.css | 17 +- 5 files changed, 593 insertions(+), 584 deletions(-) diff --git a/src/api.ts b/src/api.ts index 4bb1e53..a043c6a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,130 +1,132 @@ -import { requestUrl, RequestUrlParam } from 'obsidian'; -import { BackendResponse, Recording, TranscriptSegment, SummaryData, StudyReportData, SonicNotePluginSettings } from './types'; +import { requestUrl } from 'obsidian'; +import { BackendResponse, Recording, TranscriptSegment, SummaryData, StudyReportData } from './types'; export class SonicNoteApiClient { - constructor(private getSettings: () => SonicNotePluginSettings) {} + constructor(private getSettings: () => { token: string; serverUrl: string }) {} - isAuthenticated(): boolean { - return this.getSettings().token !== ''; - } + isAuthenticated(): boolean { + return this.getSettings().token !== ''; + } - private get serverUrl(): string { - return this.getSettings().serverUrl; - } + private get serverUrl(): string { + return this.getSettings().serverUrl; + } - private get token(): string { - return this.getSettings().token; - } + private get token(): string { + return this.getSettings().token; + } - private async request(method: 'GET' | 'POST', path: string, options?: { query?: Record; body?: unknown }): Promise { - let url = `${this.serverUrl}${path}`; - if (options?.query) { - const params = new URLSearchParams(); - for (const [k, v] of Object.entries(options.query)) { - if (v !== undefined && v !== null) params.set(k, String(v)); - } - url += '?' + params.toString(); - } + private async request(method: 'GET' | 'POST', path: string, options?: { query?: Record; body?: unknown }): Promise { + let url = `${this.serverUrl}${path}`; + if (options?.query) { + const params = new URLSearchParams(); + for (const [k, v] of Object.entries(options.query)) { + if (v !== undefined && v !== null) params.set(k, String(v)); + } + url += '?' + params.toString(); + } - const headers: Record = {}; - if (this.token) { - headers['Authorization'] = `Bearer ${this.token}`; - } + const headers: Record = {}; + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } - if (method === 'POST' && options?.body) { - headers['Content-Type'] = 'application/json'; - } + if (method === 'POST' && options?.body) { + headers['Content-Type'] = 'application/json'; + } - try { - const response = await requestUrl({ - url, - method, - headers, - body: method === 'POST' && options?.body ? JSON.stringify(options.body) : undefined, - }); - return response.json as BackendResponse; - } catch (e: any) { - // requestUrl throws on non-2xx status codes; try to extract the body - if (e?.json) { - return e.json as BackendResponse; - } - console.error(`[SonicNote] 请求失败 ${method} ${path}:`, e?.message || e); - throw new Error(e?.message || `请求失败: ${method} ${path}`); - } - } + try { + const response = await requestUrl({ + url, + method, + headers, + body: method === 'POST' && options?.body ? JSON.stringify(options.body) : undefined, + }); + return response.json as BackendResponse; + } catch (e: unknown) { + const err = e as { json?: BackendResponse; message?: string }; + if (err?.json) { + return err.json as BackendResponse; + } + console.error(`[SonicNote] 请求失败 ${method} ${path}:`, err?.message || e); + throw new Error(err?.message || `请求失败: ${method} ${path}`); + } + } - async login(apiKey: string): Promise<{ token: string; userId: string }> { - const res = await this.request('POST', '/app/mcp/login', { - body: { apiKey }, - }); - if (res.code !== 200) { - throw new Error(res.msg || '登录失败'); - } - const data = res.data; - const token = typeof data === 'string' ? data : data?.token; - if (!token) { - throw new Error('登录响应中缺少 token'); - } - return { - token, - userId: data?.user?.userId || data?.userId || '', - }; - } + async login(apiKey: string): Promise<{ token: string; userId: string }> { + const res = await this.request('POST', '/app/mcp/login', { + body: { apiKey }, + }); + if (res.code !== 200) { + throw new Error(res.msg || '登录失败'); + } + const data = res.data as { token?: string; user?: { userId: string }; userId?: string }; + const token = typeof data === 'string' ? data : data?.token; + if (!token) { + throw new Error('登录响应中缺少 token'); + } + return { + token, + userId: data?.user?.userId || data?.userId || '', + }; + } - async fetchRecordingList(page: number, size: number): Promise<{ list: Recording[]; total: number }> { - const res = await this.request('GET', '/app/recording/list', { - query: { page, size }, - }); - if (res.code !== 200) { - throw new Error(res.msg || '获取录音列表失败'); - } - return { - list: res.data?.records || res.data?.list || [], - total: res.data?.total || 0, - }; - } + async fetchRecordingList(page: number, size: number): Promise<{ list: Recording[]; total: number }> { + const res = await this.request('GET', '/app/recording/list', { + query: { page, size }, + }); + if (res.code !== 200) { + throw new Error(res.msg || '获取录音列表失败'); + } + const data = res.data as { records?: Recording[]; list?: Recording[]; total?: number }; + return { + list: data?.records || data?.list || [], + total: data?.total || 0, + }; + } - async fetchRecordingDetail(audioId: string): Promise { - const res = await this.request('GET', '/app/recording/detail', { - query: { audioId }, - }); - if (res.code !== 200) { - throw new Error(res.msg || '获取录音详情失败'); - } - return res.data; - } + async fetchRecordingDetail(audioId: string): Promise { + const res = await this.request('GET', '/app/recording/detail', { + query: { audioId }, + }); + if (res.code !== 200) { + throw new Error(res.msg || '获取录音详情失败'); + } + return res.data as Recording; + } - async fetchNote(audioId: string): Promise { - const res = await this.request('GET', '/app/recording/getNote', { - query: { audioId }, - }); - if (res.code !== 200) { - return ''; - } - return res.data?.note || ''; - } + async fetchNote(audioId: string): Promise { + const res = await this.request('GET', '/app/recording/getNote', { + query: { audioId }, + }); + if (res.code !== 200) { + return ''; + } + const data = res.data as { note?: string }; + return data?.note || ''; + } - async fetchTranscriptResult(audioId: string): Promise { - const res = await this.request('GET', `/share/${audioId}/transcript/result`); - if (res.code !== 200) { - return []; - } - return res.data || []; - } + async fetchTranscriptResult(audioId: string): Promise { + const res = await this.request('GET', `/share/${audioId}/transcript/result`); + if (res.code !== 200) { + return []; + } + return (res.data as TranscriptSegment[]) || []; + } - async fetchSummary(audioId: string): Promise { - const res = await this.request('GET', `/share/${audioId}/summary`); - if (res.code !== 200) { - return null; - } - return res.data; - } + async fetchSummary(audioId: string): Promise { + const res = await this.request('GET', `/share/${audioId}/summary`); + if (res.code !== 200) { + return null; + } + return res.data as SummaryData; + } - async fetchStudyReport(audioId: string): Promise { - const res = await this.request('GET', `/share/${audioId}/studyReport`); - if (res.code !== 200) { - return null; - } - return res.data; - } + async fetchStudyReport(audioId: string): Promise { + const res = await this.request('GET', `/share/${audioId}/studyReport`); + if (res.code !== 200) { + return null; + } + return res.data as StudyReportData; + } } diff --git a/src/main.ts b/src/main.ts index ecbb096..547dc12 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,154 +5,154 @@ import { SonicNoteSettingTab } from './settings'; import { DEFAULT_SETTINGS, SonicNotePluginSettings } from './types'; export default class SonicNoteSyncPlugin extends Plugin { - settings: SonicNotePluginSettings = DEFAULT_SETTINGS; - private api!: SonicNoteApiClient; - private syncService!: SyncService; - private statusBarEl!: HTMLElement; - private syncTimer: number | null = null; - private syncing = false; + settings: SonicNotePluginSettings = DEFAULT_SETTINGS; + private api!: SonicNoteApiClient; + private syncService!: SyncService; + private statusBarEl!: HTMLElement; + private syncTimer: number | null = null; + private syncing = false; - async onload() { - await this.loadSettings(); + async onload() { + await this.loadSettings(); - // Initialize API client and sync service - this.api = new SonicNoteApiClient(() => this.settings); - this.syncService = new SyncService( - this.app, - this.api, - () => this.settings, - () => this.saveSettings() - ); + // Initialize API client and sync service + this.api = new SonicNoteApiClient(() => this.settings); + this.syncService = new SyncService( + this.app, + this.api, + () => this.settings, + () => this.saveSettings() + ); - // Status bar - this.statusBarEl = this.addStatusBarItem(); - this.updateStatusBar(); + // Status bar + this.statusBarEl = this.addStatusBarItem(); + this.updateStatusBar(); - // Ribbon icon - this.addRibbonIcon('headphones', 'SonicNote Sync: 同步录音', () => { - this.triggerSync(); - }); + // Ribbon icon + this.addRibbonIcon('headphones', 'SonicNote Sync: 同步录音', () => { + void this.triggerSync(); + }); - // Commands - this.addCommand({ - id: 'sonicnote-sync:sync', - name: '同步录音', - callback: () => this.triggerSync(), - }); + // Commands + this.addCommand({ + id: 'sync', + name: '同步录音', + callback: () => void this.triggerSync(), + }); - this.addCommand({ - id: 'sonicnote-sync:login', - name: '登录', - callback: () => { - // @ts-ignore - internal API to open settings - this.app.setting.open(); - // @ts-ignore - this.app.setting.openTabById('sonicnote-sync'); - }, - }); + this.addCommand({ + id: 'login', + name: '登录', + callback: () => { + // @ts-ignore - internal API to open settings + this.app.setting.open(); + // @ts-ignore + this.app.setting.openTabById('sonicnote-sync'); + }, + }); - this.addCommand({ - id: 'sonicnote-sync:logout', - name: '登出', - callback: async () => { - this.settings.token = ''; - this.settings.apiKey = ''; - await this.saveSettings(); - new Notice('已登出 SonicNote'); - this.updateStatusBar(); - }, - }); + this.addCommand({ + id: 'logout', + name: '登出', + callback: async () => { + this.settings.token = ''; + this.settings.apiKey = ''; + await this.saveSettings(); + new Notice('已登出 SonicNote'); + this.updateStatusBar(); + }, + }); - // Settings tab - this.addSettingTab(new SonicNoteSettingTab( - this.app, - this, - this.api, - () => this.settings, - () => this.saveSettings() - )); + // Settings tab + this.addSettingTab(new SonicNoteSettingTab( + this.app, + this, + this.api, + () => this.settings, + () => this.saveSettings() + )); - console.log('SonicNote Sync plugin loaded'); + console.log('SonicNote Sync plugin loaded'); - // Auto sync on open - if (this.settings.autoSyncOnOpen && this.api.isAuthenticated()) { - setTimeout(() => this.triggerSync(), 5000); - } + // Auto sync on open + if (this.settings.autoSyncOnOpen && this.api.isAuthenticated()) { + window.setTimeout(() => void this.triggerSync(), 5000); + } - // Periodic resync - this.startAutoSync(); - } + // Periodic resync + this.startAutoSync(); + } - onunload() { - this.stopAutoSync(); - console.log('SonicNote Sync plugin unloaded'); - } + onunload() { + this.stopAutoSync(); + console.log('SonicNote Sync plugin unloaded'); + } - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } - async saveSettings() { - await this.saveData(this.settings); - } + async saveSettings() { + await this.saveData(this.settings); + } - private async triggerSync() { - if (!this.api.isAuthenticated()) { - new Notice('请先登录 SonicNote(设置 → SonicNote Sync)'); - return; - } + private async triggerSync() { + if (!this.api.isAuthenticated()) { + new Notice('请先登录 SonicNote(设置 → SonicNote Sync)'); + return; + } - if (this.syncing) return; - this.syncing = true; - this.stopAutoSync(); + if (this.syncing) return; + this.syncing = true; + this.stopAutoSync(); - this.statusBarEl.setText('SonicNote: 同步中...'); + this.statusBarEl.setText('SonicNote: 同步中...'); - try { - const result = await this.syncService.syncAll((msg) => { - this.statusBarEl.setText(`SonicNote: ${msg}`); - }); + try { + const result = await this.syncService.syncAll((msg) => { + this.statusBarEl.setText(`SonicNote: ${msg}`); + }); - let message = `同步完成: ${result.synced} 条新/更新`; - if (result.skipped > 0) message += `, ${result.skipped} 条跳过`; - if (result.errors > 0) message += `, ${result.errors} 条失败`; + let message = `同步完成: ${result.synced} 条新/更新`; + if (result.skipped > 0) message += `, ${result.skipped} 条跳过`; + if (result.errors > 0) message += `, ${result.errors} 条失败`; - new Notice(message, 5000); - this.updateStatusBar(); - } catch (e) { - new Notice(`同步失败: ${e instanceof Error ? e.message : '未知错误'}`); - this.updateStatusBar(); - } finally { - this.syncing = false; - this.startAutoSync(); - } - } + new Notice(message, 5000); + this.updateStatusBar(); + } catch (e) { + new Notice(`同步失败: ${e instanceof Error ? e.message : '未知错误'}`); + this.updateStatusBar(); + } finally { + this.syncing = false; + this.startAutoSync(); + } + } - private updateStatusBar() { - if (this.api.isAuthenticated()) { - const lastSync = this.settings.lastSyncTime - ? `上次同步: ${this.settings.lastSyncTime}` - : '未同步'; - this.statusBarEl.setText(`SonicNote: ${lastSync}`); - } else { - this.statusBarEl.setText('SonicNote: 未登录'); - } - } + private updateStatusBar() { + if (this.api.isAuthenticated()) { + const lastSync = this.settings.lastSyncTime + ? `上次同步: ${this.settings.lastSyncTime}` + : '未同步'; + this.statusBarEl.setText(`SonicNote: ${lastSync}`); + } else { + this.statusBarEl.setText('SonicNote: 未登录'); + } + } - startAutoSync() { - this.stopAutoSync(); - const minutes = this.settings.resyncIntervalMinutes; - if (minutes > 0 && this.api.isAuthenticated()) { - this.syncTimer = window.setInterval(() => { - this.triggerSync(); - }, minutes * 60 * 1000); - } - } + startAutoSync() { + this.stopAutoSync(); + const minutes = this.settings.resyncIntervalMinutes; + if (minutes > 0 && this.api.isAuthenticated()) { + this.syncTimer = window.setInterval(() => { + void this.triggerSync(); + }, minutes * 60 * 1000); + } + } - stopAutoSync() { - if (this.syncTimer !== null) { - window.clearInterval(this.syncTimer); - this.syncTimer = null; - } - } + stopAutoSync() { + if (this.syncTimer !== null) { + window.clearInterval(this.syncTimer); + this.syncTimer = null; + } + } } diff --git a/src/settings.ts b/src/settings.ts index 774c79d..e7a2081 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,279 +1,283 @@ import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { SonicNoteApiClient } from './api'; -import { SonicNotePluginSettings, DEFAULT_SETTINGS, BUILTIN_FRONTMATTER_FIELDS, CustomFrontmatterField } from './types'; +import { SonicNotePluginSettings, BUILTIN_FRONTMATTER_FIELDS } from './types'; export class SonicNoteSettingTab extends PluginSettingTab { - private api: SonicNoteApiClient; - private getSettings: () => SonicNotePluginSettings; - private saveSettings: () => Promise; + private api: SonicNoteApiClient; + private getSettings: () => SonicNotePluginSettings; + private saveSettings: () => Promise; - constructor( - app: App, - plugin: Plugin, - api: SonicNoteApiClient, - getSettings: () => SonicNotePluginSettings, - saveSettings: () => Promise - ) { - super(app, plugin); - this.api = api; - this.getSettings = getSettings; - this.saveSettings = saveSettings; - } + constructor( + app: App, + plugin: Plugin, + api: SonicNoteApiClient, + getSettings: () => SonicNotePluginSettings, + saveSettings: () => Promise + ) { + super(app, plugin); + this.api = api; + this.getSettings = getSettings; + this.saveSettings = saveSettings; + } - display(): void { - const { containerEl } = this; - const settings = this.getSettings(); - containerEl.empty(); + display(): void { + const { containerEl } = this; + const settings = this.getSettings(); + containerEl.empty(); - containerEl.createEl('h2', { text: 'SonicNote Sync 设置' }); + new Setting(containerEl) + .setName('SonicNote Sync 设置') + .setHeading(); - // Sync folder - new Setting(containerEl) - .setName('同步文件夹') - .setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)') - .addText(text => text - .setPlaceholder('SonicNoteSync') - .setValue(settings.syncFolder) - .onChange(async (value) => { - settings.syncFolder = value; - await this.saveSettings(); - })); + // Sync folder + new Setting(containerEl) + .setName('同步文件夹') + .setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)') + .addText(text => text + .setPlaceholder('SonicNoteSync') + .setValue(settings.syncFolder) + .onChange(async (value) => { + settings.syncFolder = value; + await this.saveSettings(); + })); - // Include transcript - new Setting(containerEl) - .setName('包含转录内容') - .setDesc('关闭后同步的文件中不包含逐字转录内容') - .addToggle(toggle => toggle - .setValue(settings.includeTranscript) - .onChange(async (value) => { - settings.includeTranscript = value; - await this.saveSettings(); - })); + // Include transcript + new Setting(containerEl) + .setName('包含转录内容') + .setDesc('关闭后同步的文件中不包含逐字转录内容') + .addToggle(toggle => toggle + .setValue(settings.includeTranscript) + .onChange(async (value) => { + settings.includeTranscript = value; + await this.saveSettings(); + })); - // Auto sync on open - new Setting(containerEl) - .setName('启动时自动同步') - .setDesc('每次打开 Obsidian 时自动执行一次同步') - .addToggle(toggle => toggle - .setValue(settings.autoSyncOnOpen) - .onChange(async (value) => { - settings.autoSyncOnOpen = value; - await this.saveSettings(); - })); + // Auto sync on open + new Setting(containerEl) + .setName('启动时自动同步') + .setDesc('每次打开 Obsidian 时自动执行一次同步') + .addToggle(toggle => toggle + .setValue(settings.autoSyncOnOpen) + .onChange(async (value) => { + settings.autoSyncOnOpen = value; + await this.saveSettings(); + })); - // Resync interval - new Setting(containerEl) - .setName('定时重同步') - .setDesc('Obsidian 打开期间按指定间隔自动重新同步') - .addDropdown(dropdown => dropdown - .addOptions({ - '0': '关闭(手动同步)', - '60': '每 1 小时', - '180': '每 3 小时', - '360': '每 6 小时', - '1440': '每 24 小时', - }) - .setValue(String(settings.resyncIntervalMinutes)) - .onChange(async (value) => { - settings.resyncIntervalMinutes = parseInt(value, 10); - await this.saveSettings(); - const plugin = (this as any).plugin; - if (plugin?.startAutoSync) plugin.startAutoSync(); - })); + // Resync interval + new Setting(containerEl) + .setName('定时重同步') + .setDesc('Obsidian 打开期间按指定间隔自动重新同步') + .addDropdown(dropdown => dropdown + .addOptions({ + '0': '关闭(手动同步)', + '60': '每 1 小时', + '180': '每 3 小时', + '360': '每 6 小时', + '1440': '每 24 小时', + }) + .setValue(String(settings.resyncIntervalMinutes)) + .onChange(async (value) => { + settings.resyncIntervalMinutes = parseInt(value, 10); + await this.saveSettings(); + const plugin = (this as unknown as { plugin: { startAutoSync: () => void } }).plugin; + if (plugin?.startAutoSync) plugin.startAutoSync(); + })); - // Frontmatter fields toggles - const fmSection = containerEl.createDiv(); - fmSection.createEl('h3', { text: '文件属性' }); - fmSection.createEl('p', { text: '选择同步到 Frontmatter 中的属性字段', cls: 'setting-item-description' }); - fmSection.style.marginBottom = '12px'; + // Frontmatter fields toggles + new Setting(containerEl) + .setName('文件属性') + .setHeading(); - const fmListEl = fmSection.createDiv(); - const renderFrontmatterToggles = () => { - fmListEl.empty(); - for (const [key, desc] of Object.entries(BUILTIN_FRONTMATTER_FIELDS)) { - const isRequired = key === 'audio_id' || key === 'sync_time'; - if (isRequired) { - new Setting(fmListEl) - .setName(`${desc}`) - .setDesc(key) - .addText(text => { - text.setValue('必要属性').setDisabled(true); - text.inputEl.style.width = 'auto'; - text.inputEl.style.color = 'var(--text-muted)'; - text.inputEl.style.textAlign = 'center'; - text.inputEl.style.border = 'none'; - text.inputEl.style.background = 'var(--background-secondary)'; - text.inputEl.style.borderRadius = '4px'; - text.inputEl.style.padding = '2px 8px'; - text.inputEl.style.fontSize = '0.8em'; - }); - } else { - new Setting(fmListEl) - .setName(`${desc}`) - .setDesc(key) - .addToggle(toggle => { - toggle.setValue(settings.frontmatterFields[key] !== false); - toggle.onChange(async (value) => { - settings.frontmatterFields[key] = value; - await this.saveSettings(); - }); - }); - } - } - }; - renderFrontmatterToggles(); + new Setting(containerEl) + .setDesc('选择同步到 Frontmatter 中的属性字段') + .setName(''); - // Custom frontmatter fields - const customSection = containerEl.createDiv(); - customSection.createEl('h3', { text: '自定义属性' }); - customSection.createEl('p', { text: '添加自定义属性到所有同步文件的 Frontmatter 中', cls: 'setting-item-description' }); - customSection.style.marginBottom = '12px'; + const fmListEl = containerEl.createDiv({ cls: 'sonicnote-fm-list' }); + const renderFrontmatterToggles = () => { + fmListEl.empty(); + for (const [key, desc] of Object.entries(BUILTIN_FRONTMATTER_FIELDS)) { + const isRequired = key === 'audio_id' || key === 'sync_time'; + if (isRequired) { + new Setting(fmListEl) + .setName(`${desc}`) + .setDesc(key) + .addText(text => { + text.setValue('必要属性').setDisabled(true); + text.inputEl.addClass('sonicnote-required-badge'); + }); + } else { + new Setting(fmListEl) + .setName(`${desc}`) + .setDesc(key) + .addToggle(toggle => { + toggle.setValue(settings.frontmatterFields[key] !== false); + toggle.onChange(async (value) => { + settings.frontmatterFields[key] = value; + await this.saveSettings(); + }); + }); + } + } + }; + renderFrontmatterToggles(); - const customListEl = customSection.createDiv(); - const renderCustomFields = () => { - customListEl.empty(); + // Custom frontmatter fields + new Setting(containerEl) + .setName('自定义属性') + .setHeading(); - for (let i = 0; i < settings.customFrontmatter.length; i++) { - const field = settings.customFrontmatter[i]; - new Setting(customListEl) - .addText(text => text - .setPlaceholder('属性名') - .setValue(field.key) - .onChange(async (value) => { - field.key = value; - await this.saveSettings(); - })) - .addText(text => text - .setPlaceholder('属性值') - .setValue(field.value) - .onChange(async (value) => { - field.value = value; - await this.saveSettings(); - })) - .addExtraButton(btn => btn - .setIcon('trash') - .setTooltip('删除') - .onClick(async () => { - settings.customFrontmatter.splice(i, 1); - await this.saveSettings(); - renderCustomFields(); - })); - } - }; - renderCustomFields(); + new Setting(containerEl) + .setDesc('添加自定义属性到所有同步文件的 Frontmatter 中') + .setName(''); - new Setting(customSection) - .setName('添加属性') - .addButton(btn => btn - .setButtonText('+ 添加') - .onClick(async () => { - settings.customFrontmatter.push({ key: '', value: '' }); - await this.saveSettings(); - renderCustomFields(); - })); + const customListEl = containerEl.createDiv(); + const renderCustomFields = () => { + customListEl.empty(); - // Login status & actions - const loginSection = containerEl.createDiv(); - loginSection.createEl('h3', { text: '账号' }); + for (let i = 0; i < settings.customFrontmatter.length; i++) { + const field = settings.customFrontmatter[i]; + new Setting(customListEl) + .addText(text => text + .setPlaceholder('属性名') + .setValue(field.key) + .onChange(async (value) => { + field.key = value; + await this.saveSettings(); + })) + .addText(text => text + .setPlaceholder('属性值') + .setValue(field.value) + .onChange(async (value) => { + field.value = value; + await this.saveSettings(); + })) + .addExtraButton(btn => btn + .setIcon('trash') + .setTooltip('删除') + .onClick(async () => { + settings.customFrontmatter.splice(i, 1); + await this.saveSettings(); + renderCustomFields(); + })); + } + }; + renderCustomFields(); - if (this.api.isAuthenticated()) { - const maskedKey = settings.apiKey.length > 10 - ? settings.apiKey.slice(0, 10) + '...' - : settings.apiKey; - new Setting(loginSection) - .setName('登录状态') - .setDesc(`已登录: ${maskedKey}`) - .addButton(btn => btn - .setButtonText('登出') - .setWarning() - .onClick(async () => { - settings.token = ''; - settings.apiKey = ''; - await this.saveSettings(); - new Notice('已登出'); - this.display(); - })); - } else { - new Setting(loginSection) - .setName('登录') - .setDesc('使用 API Key 登录 SonicNote') - .addButton(btn => btn - .setButtonText('登录') - .onClick(() => { - new LoginModal(this.app, this.api, this.getSettings, this.saveSettings, () => this.display()).open(); - })); - } - } + new Setting(containerEl) + .setName('添加属性') + .addButton(btn => btn + .setButtonText('+ 添加') + .onClick(async () => { + settings.customFrontmatter.push({ key: '', value: '' }); + await this.saveSettings(); + renderCustomFields(); + })); + + // Login status & actions + new Setting(containerEl) + .setName('账号') + .setHeading(); + + if (this.api.isAuthenticated()) { + const maskedKey = settings.apiKey.length > 10 + ? settings.apiKey.slice(0, 10) + '...' + : settings.apiKey; + new Setting(containerEl) + .setName('登录状态') + .setDesc(`已登录: ${maskedKey}`) + .addButton(btn => btn + .setButtonText('登出') + .setWarning() + .onClick(async () => { + settings.token = ''; + settings.apiKey = ''; + await this.saveSettings(); + new Notice('已登出'); + this.display(); + })); + } else { + new Setting(containerEl) + .setName('登录') + .setDesc('使用 API Key 登录 SonicNote') + .addButton(btn => btn + .setButtonText('登录') + .onClick(() => { + new LoginModal(this.app, this.api, this.getSettings, this.saveSettings, () => this.display()).open(); + })); + } + } } class LoginModal extends Modal { - private api: SonicNoteApiClient; - private getSettings: () => SonicNotePluginSettings; - private saveSettings: () => Promise; - private onCloseCallback: () => void; + private api: SonicNoteApiClient; + private getSettings: () => SonicNotePluginSettings; + private saveSettings: () => Promise; + private onCloseCallback: () => void; - constructor( - app: App, - api: SonicNoteApiClient, - getSettings: () => SonicNotePluginSettings, - saveSettings: () => Promise, - onCloseCallback: () => void - ) { - super(app); - this.api = api; - this.getSettings = getSettings; - this.saveSettings = saveSettings; - this.onCloseCallback = onCloseCallback; - } + constructor( + app: App, + api: SonicNoteApiClient, + getSettings: () => SonicNotePluginSettings, + saveSettings: () => Promise, + onCloseCallback: () => void + ) { + super(app); + this.api = api; + this.getSettings = getSettings; + this.saveSettings = saveSettings; + this.onCloseCallback = onCloseCallback; + } - onOpen() { - const { contentEl } = this; - contentEl.empty(); + onOpen() { + const { contentEl } = this; + contentEl.empty(); - contentEl.createEl('h2', { text: '登录 SonicNote' }); + new Setting(contentEl) + .setName('登录 SonicNote') + .setHeading(); - let apiKey = ''; + let apiKey = ''; - new Setting(contentEl) - .setName('API Key') - .setDesc('在妙记 App → 我的 → API Key 管理中创建') - .addText(text => { - text - .setPlaceholder('sk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') - .onChange((value) => { apiKey = value; }); - text.inputEl.type = 'password'; - }); + new Setting(contentEl) + .setName('API Key') + .setDesc('在妙记 App → 我的 → API Key 管理中创建') + .addText(text => { + text + .setPlaceholder('sk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') + .onChange((value) => { apiKey = value; }); + text.inputEl.type = 'password'; + }); - new Setting(contentEl) - .addButton(btn => btn - .setButtonText('登录') - .setCta() - .onClick(async () => { - if (!apiKey) { - new Notice('请输入 API Key'); - return; - } - try { - btn.setButtonText('登录中...'); - btn.setDisabled(true); - const result = await this.api.login(apiKey); - const settings = this.getSettings(); - settings.token = result.token; - settings.apiKey = apiKey; - await this.saveSettings(); - new Notice('登录成功'); - this.close(); - } catch (e) { - new Notice(`登录失败: ${e instanceof Error ? e.message : '未知错误'}`); - btn.setButtonText('登录'); - btn.setDisabled(false); - } - })); - } + new Setting(contentEl) + .addButton(btn => btn + .setButtonText('登录') + .setCta() + .onClick(async () => { + if (!apiKey) { + new Notice('请输入 API Key'); + return; + } + try { + btn.setButtonText('登录中...'); + btn.setDisabled(true); + const result = await this.api.login(apiKey); + const settings = this.getSettings(); + settings.token = result.token; + settings.apiKey = apiKey; + await this.saveSettings(); + new Notice('登录成功'); + this.close(); + } catch (e) { + new Notice(`登录失败: ${e instanceof Error ? e.message : '未知错误'}`); + btn.setButtonText('登录'); + btn.setDisabled(false); + } + })); + } - onClose() { - const { contentEl } = this; - contentEl.empty(); - this.onCloseCallback(); - } + onClose() { + const { contentEl } = this; + contentEl.empty(); + this.onCloseCallback(); + } } diff --git a/src/types.ts b/src/types.ts index 23bab58..2b93403 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,126 +1,126 @@ // ===== 插件设置 ===== export interface CustomFrontmatterField { - key: string; - value: string; + key: string; + value: string; } export interface SonicNotePluginSettings { - serverUrl: string; - syncFolder: string; - pageSize: number; - includeTranscript: boolean; - autoSyncOnOpen: boolean; - resyncIntervalMinutes: number; - frontmatterFields: Record; - customFrontmatter: CustomFrontmatterField[]; - token: string; - apiKey: string; - lastSyncTime: string; + serverUrl: string; + syncFolder: string; + pageSize: number; + includeTranscript: boolean; + autoSyncOnOpen: boolean; + resyncIntervalMinutes: number; + frontmatterFields: Record; + customFrontmatter: CustomFrontmatterField[]; + token: string; + apiKey: string; + lastSyncTime: string; } export const BUILTIN_FRONTMATTER_FIELDS: Record = { - audio_id: '录音 ID', - record_name: '录音文件名', - record_nick_name: '录音标题', - duration: '时长', - record_time: '录音时间', - record_type: '录音类型', - device_name: '设备名称', - audio_url: '音频地址', - tags: '标签', - sync_time: '同步时间', + audio_id: '录音 ID', + record_name: '录音文件名', + record_nick_name: '录音标题', + duration: '时长', + record_time: '录音时间', + record_type: '录音类型', + device_name: '设备名称', + audio_url: '音频地址', + tags: '标签', + sync_time: '同步时间', }; export const DEFAULT_SETTINGS: SonicNotePluginSettings = { - serverUrl: 'https://ainote.easylinkin.com:18048/prod-api', - syncFolder: 'SonicNoteSync', - pageSize: 50, - includeTranscript: true, - autoSyncOnOpen: false, - resyncIntervalMinutes: 0, - frontmatterFields: { - audio_id: true, - record_name: true, - record_nick_name: true, - duration: true, - record_time: true, - record_type: true, - device_name: true, - audio_url: true, - tags: true, - sync_time: true, - }, - customFrontmatter: [], - token: '', - apiKey: '', - lastSyncTime: '', + serverUrl: 'https://ainote.easylinkin.com:18048/prod-api', + syncFolder: 'SonicNoteSync', + pageSize: 50, + includeTranscript: true, + autoSyncOnOpen: false, + resyncIntervalMinutes: 0, + frontmatterFields: { + audio_id: true, + record_name: true, + record_nick_name: true, + duration: true, + record_time: true, + record_type: true, + device_name: true, + audio_url: true, + tags: true, + sync_time: true, + }, + customFrontmatter: [], + token: '', + apiKey: '', + lastSyncTime: '', }; // ===== 后端数据类型 ===== export interface BackendResponse { - code: number; - msg: string; - data: any; + code: number; + msg: string; + data: unknown; } export interface Recording { - audioId: string; - userId: string; - deviceId: string; - audioUrl: string; - recordTime: string; - recordName: string; - recordNickName: string; - duration: string; - note: string; - recordType: string; - isTranscribed: string; - isSummarized: string; - transcriptStatus: number; - summaryStatus: number; - delFlag: string; - deviceName: string; - createTime: string; - updateTime: string; + audioId: string; + userId: string; + deviceId: string; + audioUrl: string; + recordTime: string; + recordName: string; + recordNickName: string; + duration: string; + note: string; + recordType: string; + isTranscribed: string; + isSummarized: string; + transcriptStatus: number; + summaryStatus: number; + delFlag: string; + deviceName: string; + createTime: string; + updateTime: string; } export interface TranscriptSegment { - spokesperson: string; - text: string; - time: number; + spokesperson: string; + text: string; + time: number; } export interface SummaryData { - summaryId: string; - audioId: string; - summaryContent: string; - templateId: string; - status: number; + summaryId: string; + audioId: string; + summaryContent: string; + templateId: string; + status: number; } export interface StudyReportData { - id: number; - audioId: string; - knowledgePanorama: string; - coreGains: string; - consolidation: string; - status: number; + id: number; + audioId: string; + knowledgePanorama: string; + coreGains: string; + consolidation: string; + status: number; } // ===== 同步相关 ===== export interface LocalFileInfo { - path: string; - syncTime: string; - recordNickName: string; + path: string; + syncTime: string; + recordNickName: string; } export interface SyncResult { - total: number; - synced: number; - skipped: number; - errors: number; - errorMessages: string[]; + total: number; + synced: number; + skipped: number; + errors: number; + errorMessages: string[]; } diff --git a/styles.css b/styles.css index 37c422d..21be544 100644 --- a/styles.css +++ b/styles.css @@ -1,10 +1,13 @@ /* SonicNote Sync Plugin Styles */ -/* Login modal */ -.sonicnote-login-modal .setting-item { - padding: 8px 0; -} - -.sonicnote-login-modal .setting-item-control input { - width: 200px; +/* Required field badge (replaces inline styles) */ +.sonicnote-required-badge { + width: auto; + color: var(--text-muted); + text-align: center; + border: none; + background: var(--background-secondary); + border-radius: 4px; + padding: 2px 8px; + font-size: 0.8em; }