mirror of
https://github.com/long2icc/sonicnote-sync.git
synced 2026-07-22 07:45:14 +00:00
style: 优化代码格式和样式,调整设置界面和登录模态样式
This commit is contained in:
parent
e0a7399349
commit
d6245a74d3
5 changed files with 593 additions and 584 deletions
228
src/api.ts
228
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<string, string | number>; body?: unknown }): Promise<BackendResponse> {
|
||||
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<string, string | number>; body?: unknown }): Promise<BackendResponse> {
|
||||
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<string, string> = {};
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
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<Recording> {
|
||||
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<Recording> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<TranscriptSegment[]> {
|
||||
const res = await this.request('GET', `/share/${audioId}/transcript/result`);
|
||||
if (res.code !== 200) {
|
||||
return [];
|
||||
}
|
||||
return res.data || [];
|
||||
}
|
||||
async fetchTranscriptResult(audioId: string): Promise<TranscriptSegment[]> {
|
||||
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<SummaryData | null> {
|
||||
const res = await this.request('GET', `/share/${audioId}/summary`);
|
||||
if (res.code !== 200) {
|
||||
return null;
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
async fetchSummary(audioId: string): Promise<SummaryData | null> {
|
||||
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<StudyReportData | null> {
|
||||
const res = await this.request('GET', `/share/${audioId}/studyReport`);
|
||||
if (res.code !== 200) {
|
||||
return null;
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
async fetchStudyReport(audioId: string): Promise<StudyReportData | null> {
|
||||
const res = await this.request('GET', `/share/${audioId}/studyReport`);
|
||||
if (res.code !== 200) {
|
||||
return null;
|
||||
}
|
||||
return res.data as StudyReportData;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
254
src/main.ts
254
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
502
src/settings.ts
502
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<void>;
|
||||
private api: SonicNoteApiClient;
|
||||
private getSettings: () => SonicNotePluginSettings;
|
||||
private saveSettings: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: Plugin,
|
||||
api: SonicNoteApiClient,
|
||||
getSettings: () => SonicNotePluginSettings,
|
||||
saveSettings: () => Promise<void>
|
||||
) {
|
||||
super(app, plugin);
|
||||
this.api = api;
|
||||
this.getSettings = getSettings;
|
||||
this.saveSettings = saveSettings;
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: Plugin,
|
||||
api: SonicNoteApiClient,
|
||||
getSettings: () => SonicNotePluginSettings,
|
||||
saveSettings: () => Promise<void>
|
||||
) {
|
||||
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<void>;
|
||||
private onCloseCallback: () => void;
|
||||
private api: SonicNoteApiClient;
|
||||
private getSettings: () => SonicNotePluginSettings;
|
||||
private saveSettings: () => Promise<void>;
|
||||
private onCloseCallback: () => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
api: SonicNoteApiClient,
|
||||
getSettings: () => SonicNotePluginSettings,
|
||||
saveSettings: () => Promise<void>,
|
||||
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<void>,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
src/types.ts
176
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<string, boolean>;
|
||||
customFrontmatter: CustomFrontmatterField[];
|
||||
token: string;
|
||||
apiKey: string;
|
||||
lastSyncTime: string;
|
||||
serverUrl: string;
|
||||
syncFolder: string;
|
||||
pageSize: number;
|
||||
includeTranscript: boolean;
|
||||
autoSyncOnOpen: boolean;
|
||||
resyncIntervalMinutes: number;
|
||||
frontmatterFields: Record<string, boolean>;
|
||||
customFrontmatter: CustomFrontmatterField[];
|
||||
token: string;
|
||||
apiKey: string;
|
||||
lastSyncTime: string;
|
||||
}
|
||||
|
||||
export const BUILTIN_FRONTMATTER_FIELDS: Record<string, string> = {
|
||||
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[];
|
||||
}
|
||||
|
|
|
|||
17
styles.css
17
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue