🎨 Optimize storage logic

This commit is contained in:
2025-07-13 22:32:58 +08:00
parent 6d8fdf62f1
commit b4b0ad9bba
11 changed files with 671 additions and 45 deletions

View File

@@ -33,6 +33,11 @@ export class AppConfig {
*/ */
"updates": UpdatesConfig; "updates": UpdatesConfig;
/**
* Git同步设置
*/
"sync": GitSyncConfig;
/** /**
* 配置元数据 * 配置元数据
*/ */
@@ -52,6 +57,9 @@ export class AppConfig {
if (!("updates" in $$source)) { if (!("updates" in $$source)) {
this["updates"] = (new UpdatesConfig()); this["updates"] = (new UpdatesConfig());
} }
if (!("sync" in $$source)) {
this["sync"] = (new GitSyncConfig());
}
if (!("metadata" in $$source)) { if (!("metadata" in $$source)) {
this["metadata"] = (new ConfigMetadata()); this["metadata"] = (new ConfigMetadata());
} }
@@ -68,6 +76,7 @@ export class AppConfig {
const $$createField2_0 = $$createType2; const $$createField2_0 = $$createType2;
const $$createField3_0 = $$createType3; const $$createField3_0 = $$createType3;
const $$createField4_0 = $$createType4; const $$createField4_0 = $$createType4;
const $$createField5_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("general" in $$parsedSource) { if ("general" in $$parsedSource) {
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]); $$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
@@ -81,8 +90,11 @@ export class AppConfig {
if ("updates" in $$parsedSource) { if ("updates" in $$parsedSource) {
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]); $$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
} }
if ("sync" in $$parsedSource) {
$$parsedSource["sync"] = $$createField4_0($$parsedSource["sync"]);
}
if ("metadata" in $$parsedSource) { if ("metadata" in $$parsedSource) {
$$parsedSource["metadata"] = $$createField4_0($$parsedSource["metadata"]); $$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]);
} }
return new AppConfig($$parsedSource as Partial<AppConfig>); return new AppConfig($$parsedSource as Partial<AppConfig>);
} }
@@ -126,7 +138,7 @@ export class AppearanceConfig {
* Creates a new AppearanceConfig instance from a string or object. * Creates a new AppearanceConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): AppearanceConfig { static createFrom($$source: any = {}): AppearanceConfig {
const $$createField2_0 = $$createType5; const $$createField2_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("customTheme" in $$parsedSource) { if ("customTheme" in $$parsedSource) {
$$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]); $$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]);
@@ -135,6 +147,31 @@ export class AppearanceConfig {
} }
} }
/**
* AuthMethod 定义Git认证方式
*/
export enum AuthMethod {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 个人访问令牌
*/
Token = "token",
/**
* SSH密钥
*/
SSHKey = "ssh_key",
/**
* 用户名密码
*/
UserPass = "user_pass",
};
/** /**
* ConfigMetadata 配置元数据 * ConfigMetadata 配置元数据
*/ */
@@ -200,8 +237,8 @@ export class CustomThemeConfig {
* Creates a new CustomThemeConfig instance from a string or object. * Creates a new CustomThemeConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): CustomThemeConfig { static createFrom($$source: any = {}): CustomThemeConfig {
const $$createField0_0 = $$createType6; const $$createField0_0 = $$createType7;
const $$createField1_0 = $$createType6; const $$createField1_0 = $$createType7;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("darkTheme" in $$parsedSource) { if ("darkTheme" in $$parsedSource) {
$$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]); $$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]);
@@ -224,6 +261,11 @@ export class Document {
"updatedAt": time$0.Time; "updatedAt": time$0.Time;
"is_deleted": boolean; "is_deleted": boolean;
/**
* 锁定标志,锁定的文档无法被删除
*/
"is_locked": boolean;
/** Creates a new Document instance. */ /** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) { constructor($$source: Partial<Document> = {}) {
if (!("id" in $$source)) { if (!("id" in $$source)) {
@@ -244,6 +286,9 @@ export class Document {
if (!("is_deleted" in $$source)) { if (!("is_deleted" in $$source)) {
this["is_deleted"] = false; this["is_deleted"] = false;
} }
if (!("is_locked" in $$source)) {
this["is_locked"] = false;
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }
@@ -389,7 +434,7 @@ export class Extension {
* Creates a new Extension instance from a string or object. * Creates a new Extension instance from a string or object.
*/ */
static createFrom($$source: any = {}): Extension { static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType7; const $$createField3_0 = $$createType8;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) { if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]); $$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
@@ -522,7 +567,7 @@ export class GeneralConfig {
* Creates a new GeneralConfig instance from a string or object. * Creates a new GeneralConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): GeneralConfig { static createFrom($$source: any = {}): GeneralConfig {
const $$createField5_0 = $$createType9; const $$createField5_0 = $$createType10;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("globalHotkey" in $$parsedSource) { if ("globalHotkey" in $$parsedSource) {
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
@@ -531,6 +576,91 @@ export class GeneralConfig {
} }
} }
/**
* GitSyncConfig 保存Git同步的配置信息
*/
export class GitSyncConfig {
"enabled": boolean;
"repo_url": string;
"branch": string;
"auth_method": AuthMethod;
"username"?: string;
"password"?: string;
"token"?: string;
"ssh_key_path"?: string;
"ssh_key_passphrase"?: string;
/**
* 同步间隔(分钟)
*/
"sync_interval": number;
"last_sync_time": time$0.Time;
/**
* 是否启用自动同步
*/
"auto_sync": boolean;
"local_repo_path": string;
/**
* 合并冲突策略
*/
"sync_strategy": SyncStrategy;
/**
* 要同步的文件列表,默认为数据库文件
*/
"files_to_sync": string[];
/** Creates a new GitSyncConfig instance. */
constructor($$source: Partial<GitSyncConfig> = {}) {
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("repo_url" in $$source)) {
this["repo_url"] = "";
}
if (!("branch" in $$source)) {
this["branch"] = "";
}
if (!("auth_method" in $$source)) {
this["auth_method"] = ("" as AuthMethod);
}
if (!("sync_interval" in $$source)) {
this["sync_interval"] = 0;
}
if (!("last_sync_time" in $$source)) {
this["last_sync_time"] = null;
}
if (!("auto_sync" in $$source)) {
this["auto_sync"] = false;
}
if (!("local_repo_path" in $$source)) {
this["local_repo_path"] = "";
}
if (!("sync_strategy" in $$source)) {
this["sync_strategy"] = ("" as SyncStrategy);
}
if (!("files_to_sync" in $$source)) {
this["files_to_sync"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new GitSyncConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GitSyncConfig {
const $$createField14_0 = $$createType11;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("files_to_sync" in $$parsedSource) {
$$parsedSource["files_to_sync"] = $$createField14_0($$parsedSource["files_to_sync"]);
}
return new GitSyncConfig($$parsedSource as Partial<GitSyncConfig>);
}
}
/** /**
* GiteaConfig Gitea配置 * GiteaConfig Gitea配置
*/ */
@@ -1038,6 +1168,26 @@ export enum LanguageType {
LangEnUS = "en-US", LangEnUS = "en-US",
}; };
/**
* SyncStrategy 定义同步策略
*/
export enum SyncStrategy {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* LocalFirst 本地优先:如有冲突,保留本地修改
*/
LocalFirst = "local_first",
/**
* RemoteFirst 远程优先:如有冲突,采用远程版本
*/
RemoteFirst = "remote_first",
};
/** /**
* SystemThemeType 系统主题类型定义 * SystemThemeType 系统主题类型定义
*/ */
@@ -1389,8 +1539,8 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object. * Creates a new UpdatesConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): UpdatesConfig { static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType10; const $$createField6_0 = $$createType12;
const $$createField7_0 = $$createType11; const $$createField7_0 = $$createType13;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) { if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
@@ -1407,16 +1557,18 @@ const $$createType0 = GeneralConfig.createFrom;
const $$createType1 = EditingConfig.createFrom; const $$createType1 = EditingConfig.createFrom;
const $$createType2 = AppearanceConfig.createFrom; const $$createType2 = AppearanceConfig.createFrom;
const $$createType3 = UpdatesConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom;
const $$createType4 = ConfigMetadata.createFrom; const $$createType4 = GitSyncConfig.createFrom;
const $$createType5 = CustomThemeConfig.createFrom; const $$createType5 = ConfigMetadata.createFrom;
const $$createType6 = ThemeColorConfig.createFrom; const $$createType6 = CustomThemeConfig.createFrom;
var $$createType7 = (function $$initCreateType7(...args): any { const $$createType7 = ThemeColorConfig.createFrom;
if ($$createType7 === $$initCreateType7) { var $$createType8 = (function $$initCreateType8(...args): any {
$$createType7 = $$createType8; if ($$createType8 === $$initCreateType8) {
$$createType8 = $$createType9;
} }
return $$createType7(...args); return $$createType8(...args);
}); });
const $$createType8 = $Create.Map($Create.Any, $Create.Any); const $$createType9 = $Create.Map($Create.Any, $Create.Any);
const $$createType9 = HotkeyCombo.createFrom; const $$createType10 = HotkeyCombo.createFrom;
const $$createType10 = GithubConfig.createFrom; const $$createType11 = $Create.Array($Create.Any);
const $$createType11 = GiteaConfig.createFrom; const $$createType12 = GithubConfig.createFrom;
const $$createType13 = GiteaConfig.createFrom;

View File

@@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
return $resultPromise; return $resultPromise;
} }
/**
* RegisterModel 注册模型与表的映射关系
*/
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
return $resultPromise;
}
/** /**
* ServiceShutdown shuts down the service when the application closes * ServiceShutdown shuts down the service when the application closes
*/ */

View File

@@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]
return $typingPromise; return $typingPromise;
} }
/**
* LockDocument 锁定文档,防止删除
*/
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any;
return $resultPromise;
}
/** /**
* RestoreDocument restores a deleted document * RestoreDocument restores a deleted document
*/ */
@@ -97,6 +105,14 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
return $resultPromise; return $resultPromise;
} }
/**
* UnlockDocument 解锁文档
*/
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(222307930, id) as any;
return $resultPromise;
}
/** /**
* UpdateDocumentContent updates the content of a document * UpdateDocumentContent updates the content of a document
*/ */

View File

@@ -11,6 +11,9 @@ import {
TabType, TabType,
UpdatesConfig, UpdatesConfig,
UpdateSourceType, UpdateSourceType,
GitSyncConfig,
AuthMethod,
SyncStrategy
} from '@/../bindings/voidraft/internal/models/models'; } from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/utils/configUtils'; import {ConfigUtils} from '@/utils/configUtils';
@@ -48,6 +51,10 @@ type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string; readonly [K in keyof UpdatesConfig]: string;
}; };
type SyncConfigKeyMap = {
readonly [K in keyof GitSyncConfig]: string;
};
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
// 配置键映射 // 配置键映射
@@ -88,6 +95,24 @@ const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
gitea: 'updates.gitea' gitea: 'updates.gitea'
} as const; } as const;
const SYNC_CONFIG_KEY_MAP: SyncConfigKeyMap = {
enabled: 'sync.enabled',
repo_url: 'sync.repo_url',
branch: 'sync.branch',
auth_method: 'sync.auth_method',
username: 'sync.username',
password: 'sync.password',
token: 'sync.token',
ssh_key_path: 'sync.ssh_key_path',
ssh_key_passphrase: 'sync.ssh_key_passphrase',
sync_interval: 'sync.sync_interval',
last_sync_time: 'sync.last_sync_time',
auto_sync: 'sync.auto_sync',
local_repo_path: 'sync.local_repo_path',
sync_strategy: 'sync.sync_strategy',
files_to_sync: 'sync.files_to_sync'
} as const;
// 配置限制 // 配置限制
const CONFIG_LIMITS = { const CONFIG_LIMITS = {
fontSize: {min: 12, max: 28, default: 13}, fontSize: {min: 12, max: 28, default: 13},
@@ -261,6 +286,23 @@ const DEFAULT_CONFIG: AppConfig = {
repo: "voidraft", repo: "voidraft",
} }
}, },
sync: {
enabled: false,
repo_url: "",
branch: "main",
auth_method: AuthMethod.Token,
username: "",
password: "",
token: "",
ssh_key_path: "",
ssh_key_passphrase: "",
sync_interval: 60,
last_sync_time: null,
auto_sync: true,
local_repo_path: "",
sync_strategy: SyncStrategy.LocalFirst,
files_to_sync: ["voidraft.db"]
},
metadata: { metadata: {
version: '1.0.0', version: '1.0.0',
lastUpdated: new Date().toString(), lastUpdated: new Date().toString(),
@@ -348,6 +390,21 @@ export const useConfigStore = defineStore('config', () => {
state.config.updates[key] = value; state.config.updates[key] = value;
}; };
const updateSyncConfig = async <K extends keyof GitSyncConfig>(key: K, value: GitSyncConfig[K]): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
const backendKey = SYNC_CONFIG_KEY_MAP[key];
if (!backendKey) {
throw new Error(`No backend key mapping found for sync.${key.toString()}`);
}
await ConfigService.Set(backendKey, value);
state.config.sync[key] = value;
};
// 加载配置 // 加载配置
const initConfig = async (): Promise<void> => { const initConfig = async (): Promise<void> => {
if (state.isLoading) return; if (state.isLoading) return;
@@ -362,6 +419,7 @@ export const useConfigStore = defineStore('config', () => {
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing); if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance); if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates); if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
if (appConfig.sync) Object.assign(state.config.sync, appConfig.sync);
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata); if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
} }
@@ -594,6 +652,20 @@ export const useConfigStore = defineStore('config', () => {
}, },
// 更新配置相关方法 // 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value) setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
// Git同步配置相关方法
setGitSyncEnabled: (value: boolean) => updateSyncConfig('enabled', value),
setGitRepoUrl: (value: string) => updateSyncConfig('repo_url', value),
setGitBranch: (value: string) => updateSyncConfig('branch', value),
setAuthMethod: (value: AuthMethod) => updateSyncConfig('auth_method', value),
setGitUsername: (value: string) => updateSyncConfig('username', value),
setGitPassword: (value: string) => updateSyncConfig('password', value),
setGitToken: (value: string) => updateSyncConfig('token', value),
setSSHKeyPath: (value: string) => updateSyncConfig('ssh_key_path', value),
setSyncInterval: (value: number) => updateSyncConfig('sync_interval', value),
setAutoSync: (value: boolean) => updateSyncConfig('auto_sync', value),
setSyncStrategy: (value: SyncStrategy) => updateSyncConfig('sync_strategy', value),
setFilesToSync: (value: string[]) => updateSyncConfig('files_to_sync', value),
}; };
}); });

View File

@@ -124,6 +124,7 @@ type AppConfig struct {
Editing EditingConfig `json:"editing"` // 编辑设置 Editing EditingConfig `json:"editing"` // 编辑设置
Appearance AppearanceConfig `json:"appearance"` // 外观设置 Appearance AppearanceConfig `json:"appearance"` // 外观设置
Updates UpdatesConfig `json:"updates"` // 更新设置 Updates UpdatesConfig `json:"updates"` // 更新设置
Sync GitSyncConfig `json:"sync"` // Git同步设置
Metadata ConfigMetadata `json:"metadata"` // 配置元数据 Metadata ConfigMetadata `json:"metadata"` // 配置元数据
} }
@@ -138,6 +139,7 @@ func NewDefaultAppConfig() *AppConfig {
currentDir, _ := os.UserHomeDir() currentDir, _ := os.UserHomeDir()
dataDir := filepath.Join(currentDir, ".voidraft", "data") dataDir := filepath.Join(currentDir, ".voidraft", "data")
repoPath := filepath.Join(currentDir, ".voidraft", "sync-repo")
return &AppConfig{ return &AppConfig{
General: GeneralConfig{ General: GeneralConfig{
@@ -173,7 +175,7 @@ func NewDefaultAppConfig() *AppConfig {
CustomTheme: *NewDefaultCustomThemeConfig(), CustomTheme: *NewDefaultCustomThemeConfig(),
}, },
Updates: UpdatesConfig{ Updates: UpdatesConfig{
Version: "1.0.0", Version: "1.2.0",
AutoUpdate: true, AutoUpdate: true,
PrimarySource: UpdateSourceGithub, PrimarySource: UpdateSourceGithub,
BackupSource: UpdateSourceGitea, BackupSource: UpdateSourceGitea,
@@ -189,9 +191,25 @@ func NewDefaultAppConfig() *AppConfig {
Repo: "voidraft", Repo: "voidraft",
}, },
}, },
Sync: GitSyncConfig{
Enabled: false,
RepoURL: "",
Branch: "main",
AuthMethod: Token,
Username: "",
Password: "",
Token: "",
SSHKeyPath: "",
SyncInterval: 60,
LastSyncTime: time.Time{},
AutoSync: true,
LocalRepoPath: repoPath,
SyncStrategy: LocalFirst,
FilesToSync: []string{"voidraft.db"},
},
Metadata: ConfigMetadata{ Metadata: ConfigMetadata{
LastUpdated: time.Now().Format(time.RFC3339), LastUpdated: time.Now().Format(time.RFC3339),
Version: "1.0.0", Version: "1.2.0",
}, },
} }
} }

View File

@@ -12,9 +12,10 @@ type Document struct {
CreatedAt time.Time `json:"createdAt" db:"created_at"` CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
IsLocked bool `json:"is_locked" db:"is_locked"` // 锁定标志,锁定的文档无法被删除
} }
// NewDocument 创建新文档不需要传ID由数据库自增 // NewDocument 创建新文档
func NewDocument(title, content string) *Document { func NewDocument(title, content string) *Document {
now := time.Now() now := time.Now()
return &Document{ return &Document{
@@ -23,6 +24,7 @@ func NewDocument(title, content string) *Document {
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
IsDeleted: false, IsDeleted: false,
IsLocked: false, // 默认不锁定
} }
} }

View File

@@ -4,10 +4,10 @@ import "time"
// Extension 单个扩展配置 // Extension 单个扩展配置
type Extension struct { type Extension struct {
ID ExtensionID `json:"id"` // 扩展唯一标识 ID ExtensionID `json:"id" db:"id"` // 扩展唯一标识
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled" db:"enabled"` // 是否启用
IsDefault bool `json:"isDefault"` // 是否为默认扩展 IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认扩展
Config ExtensionConfig `json:"config"` // 扩展配置项 Config ExtensionConfig `json:"config" db:"config"` // 扩展配置项
} }
// ExtensionID 扩展标识符 // ExtensionID 扩展标识符

View File

@@ -4,11 +4,11 @@ import "time"
// KeyBinding 单个快捷键绑定 // KeyBinding 单个快捷键绑定
type KeyBinding struct { type KeyBinding struct {
Command KeyBindingCommand `json:"command"` // 快捷键动作 Command KeyBindingCommand `json:"command" db:"command"` // 快捷键动作
Extension ExtensionID `json:"extension"` // 所属扩展 Extension ExtensionID `json:"extension" db:"extension"` // 所属扩展
Key string `json:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p" Key string `json:"key" db:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p"
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled" db:"enabled"` // 是否启用
IsDefault bool `json:"isDefault"` // 是否为默认快捷键 IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认快捷键
} }
// KeyBindingCommand 快捷键命令 // KeyBindingCommand 快捷键命令

63
internal/models/sync.go Normal file
View File

@@ -0,0 +1,63 @@
package models
import "time"
// AuthMethod 定义Git认证方式
type AuthMethod string
const (
Token AuthMethod = "token" // 个人访问令牌
SSHKey AuthMethod = "ssh_key" // SSH密钥
UserPass AuthMethod = "user_pass" // 用户名密码
)
// SyncStrategy 定义同步策略
type SyncStrategy string
const (
// LocalFirst 本地优先:如有冲突,保留本地修改
LocalFirst SyncStrategy = "local_first"
// RemoteFirst 远程优先:如有冲突,采用远程版本
RemoteFirst SyncStrategy = "remote_first"
)
// GitSyncConfig 保存Git同步的配置信息
type GitSyncConfig struct {
Enabled bool `json:"enabled"`
RepoURL string `json:"repo_url"`
Branch string `json:"branch"`
AuthMethod AuthMethod `json:"auth_method"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
SSHKeyPath string `json:"ssh_key_path,omitempty"`
SSHKeyPassphrase string `json:"ssh_key_passphrase,omitempty"`
SyncInterval int `json:"sync_interval"` // 同步间隔(分钟)
LastSyncTime time.Time `json:"last_sync_time"`
AutoSync bool `json:"auto_sync"` // 是否启用自动同步
LocalRepoPath string `json:"local_repo_path"`
SyncStrategy SyncStrategy `json:"sync_strategy"` // 合并冲突策略
FilesToSync []string `json:"files_to_sync"` // 要同步的文件列表,默认为数据库文件
}
// GitSyncStatus 保存同步状态信息
type GitSyncStatus struct {
IsSyncing bool `json:"is_syncing"`
LastSyncTime time.Time `json:"last_sync_time"`
LastSyncStatus string `json:"last_sync_status"` // success, failed, conflict
LastErrorMsg string `json:"last_error_msg,omitempty"`
LastCommitID string `json:"last_commit_id,omitempty"`
RemoteCommitID string `json:"remote_commit_id,omitempty"`
CommitAhead int `json:"commit_ahead"` // 本地领先远程的提交数
CommitBehind int `json:"commit_behind"` // 本地落后远程的提交数
}
// SyncLogEntry 记录每次同步操作的日志
type SyncLogEntry struct {
ID int64 `json:"id"`
Timestamp time.Time `json:"timestamp"`
Action string `json:"action"` // push, pull, reset
Status string `json:"status"` // success, failed
Message string `json:"message,omitempty"`
ChangedFiles int `json:"changed_files"`
}

View File

@@ -5,7 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings"
"sync" "sync"
"time"
"voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
@@ -31,7 +35,8 @@ CREATE TABLE IF NOT EXISTS documents (
content TEXT DEFAULT '∞∞∞text-a', content TEXT DEFAULT '∞∞∞text-a',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0 is_deleted INTEGER DEFAULT 0,
is_locked INTEGER DEFAULT 0
)` )`
// Extensions table // Extensions table
@@ -58,8 +63,34 @@ CREATE TABLE IF NOT EXISTS key_bindings (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(command, extension) UNIQUE(command, extension)
)` )`
// Git sync logs table
sqlCreateSyncLogsTable = `
CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
action TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
commit_id TEXT,
changed_files INTEGER DEFAULT 0,
repo_url TEXT NOT NULL,
branch TEXT NOT NULL
)`
) )
// ColumnInfo 存储列的信息
type ColumnInfo struct {
SQLType string
DefaultValue string
}
// TableModel 表示表与模型之间的映射关系
type TableModel struct {
TableName string
Model interface{}
}
// DatabaseService provides shared database functionality // DatabaseService provides shared database functionality
type DatabaseService struct { type DatabaseService struct {
configService *ConfigService configService *ConfigService
@@ -67,6 +98,7 @@ type DatabaseService struct {
SQLite *sqlite.Service SQLite *sqlite.Service
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
tableModels []TableModel // 注册的表模型
} }
// NewDatabaseService creates a new database service // NewDatabaseService creates a new database service
@@ -75,11 +107,28 @@ func NewDatabaseService(configService *ConfigService, logger *log.Service) *Data
logger = log.New() logger = log.New()
} }
return &DatabaseService{ ds := &DatabaseService{
configService: configService, configService: configService,
logger: logger, logger: logger,
SQLite: sqlite.New(), SQLite: sqlite.New(),
} }
// 注册所有模型
ds.registerAllModels()
return ds
}
// registerAllModels 注册所有数据模型,集中管理表-模型映射
func (ds *DatabaseService) registerAllModels() {
// 文档表
ds.RegisterModel("documents", &models.Document{})
// 扩展表
ds.RegisterModel("extensions", &models.Extension{})
// 快捷键表
ds.RegisterModel("key_bindings", &models.KeyBinding{})
// 同步日志表
ds.RegisterModel("sync_logs", &models.SyncLogEntry{})
} }
// ServiceStartup initializes the service when the application starts // ServiceStartup initializes the service when the application starts
@@ -135,6 +184,11 @@ func (ds *DatabaseService) initDatabase() error {
return fmt.Errorf("failed to create indexes: %w", err) return fmt.Errorf("failed to create indexes: %w", err)
} }
// 执行模型与表结构同步
if err := ds.syncAllModelTables(); err != nil {
return fmt.Errorf("failed to sync model tables: %w", err)
}
return nil return nil
} }
@@ -153,6 +207,7 @@ func (ds *DatabaseService) createTables() error {
sqlCreateDocumentsTable, sqlCreateDocumentsTable,
sqlCreateExtensionsTable, sqlCreateExtensionsTable,
sqlCreateKeyBindingsTable, sqlCreateKeyBindingsTable,
sqlCreateSyncLogsTable,
} }
for _, table := range tables { for _, table := range tables {
@@ -176,6 +231,10 @@ func (ds *DatabaseService) createIndexes() error {
`CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`,
`CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`,
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
// Sync logs indexes
`CREATE INDEX IF NOT EXISTS idx_sync_logs_timestamp ON sync_logs(timestamp DESC)`,
`CREATE INDEX IF NOT EXISTS idx_sync_logs_action ON sync_logs(action)`,
`CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status)`,
} }
for _, index := range indexes { for _, index := range indexes {
@@ -186,6 +245,149 @@ func (ds *DatabaseService) createIndexes() error {
return nil return nil
} }
// RegisterModel 注册模型与表的映射关系
func (ds *DatabaseService) RegisterModel(tableName string, model interface{}) {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.tableModels = append(ds.tableModels, TableModel{
TableName: tableName,
Model: model,
})
}
// syncAllModelTables 同步所有注册的模型与表结构
func (ds *DatabaseService) syncAllModelTables() error {
for _, tm := range ds.tableModels {
if err := ds.syncModelTable(tm.TableName, tm.Model); err != nil {
return fmt.Errorf("failed to sync table %s: %w", tm.TableName, err)
}
}
return nil
}
// syncModelTable 同步模型与表结构
func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) error {
// 获取表结构元数据
columns, err := ds.getTableColumns(tableName)
if err != nil {
return fmt.Errorf("failed to get table columns: %w", err)
}
// 使用反射从模型中提取字段信息
expectedColumns, err := ds.getModelColumns(model)
if err != nil {
return fmt.Errorf("failed to get model columns: %w", err)
}
// 检查缺失的列并添加
for colName, colInfo := range expectedColumns {
if _, exists := columns[colName]; !exists {
// 执行添加列的SQL
alterSQL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s DEFAULT %s",
tableName, colName, colInfo.SQLType, colInfo.DefaultValue)
if err := ds.SQLite.Execute(alterSQL); err != nil {
return fmt.Errorf("failed to add column %s: %w", colName, err)
}
}
}
return nil
}
// getTableColumns 获取表的列信息
func (ds *DatabaseService) getTableColumns(table string) (map[string]string, error) {
query := fmt.Sprintf("PRAGMA table_info(%s)", table)
rows, err := ds.SQLite.Query(query)
if err != nil {
return nil, err
}
columns := make(map[string]string)
for _, row := range rows {
name, ok1 := row["name"].(string)
typeName, ok2 := row["type"].(string)
if !ok1 || !ok2 {
continue
}
columns[name] = typeName
}
return columns, nil
}
// getModelColumns 从模型结构体中提取数据库列信息
func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]ColumnInfo, error) {
columns := make(map[string]ColumnInfo)
// 使用反射获取结构体的类型信息
t := reflect.TypeOf(model)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("model must be a struct or a pointer to struct")
}
// 遍历所有字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 获取数据库字段名
dbTag := field.Tag.Get("db")
if dbTag == "" {
// 如果没有db标签则使用字段名的蛇形命名方式
dbTag = toSnakeCase(field.Name)
}
// 获取字段类型对应的SQL类型和默认值
sqlType, defaultVal := getSQLTypeAndDefault(field.Type)
columns[dbTag] = ColumnInfo{
SQLType: sqlType,
DefaultValue: defaultVal,
}
}
return columns, nil
}
// toSnakeCase 将驼峰命名转换为蛇形命名
func toSnakeCase(s string) string {
var result strings.Builder
for i, r := range s {
if i > 0 && 'A' <= r && r <= 'Z' {
result.WriteRune('_')
}
result.WriteRune(r)
}
return strings.ToLower(result.String())
}
// getSQLTypeAndDefault 根据Go类型获取对应的SQL类型和默认值
func getSQLTypeAndDefault(t reflect.Type) (string, string) {
switch t.Kind() {
case reflect.Bool:
return "INTEGER", "0"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "INTEGER", "0"
case reflect.Float32, reflect.Float64:
return "REAL", "0.0"
case reflect.String:
return "TEXT", "''"
default:
// 处理特殊类型
if t == reflect.TypeOf(time.Time{}) {
return "DATETIME", "CURRENT_TIMESTAMP"
}
return "TEXT", "NULL"
}
}
// ServiceShutdown shuts down the service when the application closes // ServiceShutdown shuts down the service when the application closes
func (ds *DatabaseService) ServiceShutdown() error { func (ds *DatabaseService) ServiceShutdown() error {
return ds.SQLite.Close() return ds.SQLite.Close()

View File

@@ -18,28 +18,28 @@ const (
// Document operations // Document operations
sqlGetDocumentByID = ` sqlGetDocumentByID = `
SELECT id, title, content, created_at, updated_at, is_deleted SELECT id, title, content, created_at, updated_at, is_deleted, is_locked
FROM documents FROM documents
WHERE id = ?` WHERE id = ?`
sqlInsertDocument = ` sqlInsertDocument = `
INSERT INTO documents (title, content, created_at, updated_at, is_deleted) INSERT INTO documents (title, content, created_at, updated_at, is_deleted, is_locked)
VALUES (?, ?, ?, ?, 0)` VALUES (?, ?, ?, ?, 0, 0)`
sqlUpdateDocumentContent = ` sqlUpdateDocumentContent = `
UPDATE documents UPDATE documents
SET content = ?, updated_at = ? SET content = ?, updated_at = ?
WHERE id = ?` WHERE id = ? AND is_deleted = 0`
sqlUpdateDocumentTitle = ` sqlUpdateDocumentTitle = `
UPDATE documents UPDATE documents
SET title = ?, updated_at = ? SET title = ?, updated_at = ?
WHERE id = ?` WHERE id = ? AND is_deleted = 0`
sqlMarkDocumentAsDeleted = ` sqlMarkDocumentAsDeleted = `
UPDATE documents UPDATE documents
SET is_deleted = 1, updated_at = ? SET is_deleted = 1, updated_at = ?
WHERE id = ?` WHERE id = ? AND is_locked = 0`
sqlRestoreDocument = ` sqlRestoreDocument = `
UPDATE documents UPDATE documents
@@ -47,13 +47,13 @@ SET is_deleted = 0, updated_at = ?
WHERE id = ?` WHERE id = ?`
sqlListAllDocumentsMeta = ` sqlListAllDocumentsMeta = `
SELECT id, title, created_at, updated_at SELECT id, title, created_at, updated_at, is_locked
FROM documents FROM documents
WHERE is_deleted = 0 WHERE is_deleted = 0
ORDER BY updated_at DESC` ORDER BY updated_at DESC`
sqlListDeletedDocumentsMeta = ` sqlListDeletedDocumentsMeta = `
SELECT id, title, created_at, updated_at SELECT id, title, created_at, updated_at, is_locked
FROM documents FROM documents
WHERE is_deleted = 1 WHERE is_deleted = 1
ORDER BY updated_at DESC` ORDER BY updated_at DESC`
@@ -63,6 +63,16 @@ SELECT id FROM documents WHERE is_deleted = 0 ORDER BY id LIMIT 1`
sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0` sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0`
sqlSetDocumentLocked = `
UPDATE documents
SET is_locked = 1, updated_at = ?
WHERE id = ?`
sqlSetDocumentUnlocked = `
UPDATE documents
SET is_locked = 0, updated_at = ?
WHERE id = ?`
sqlDefaultDocumentID = 1 // 默认文档的ID sqlDefaultDocumentID = 1 // 默认文档的ID
) )
@@ -80,16 +90,19 @@ func NewDocumentService(databaseService *DatabaseService, logger *log.Service) *
logger = log.New() logger = log.New()
} }
return &DocumentService{ ds := &DocumentService{
databaseService: databaseService, databaseService: databaseService,
logger: logger, logger: logger,
} }
return ds
} }
// ServiceStartup initializes the service when the application starts // ServiceStartup initializes the service when the application starts
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
ds.ctx = ctx ds.ctx = ctx
// Ensure default document exists
// 确保默认文档存在
if err := ds.ensureDefaultDocument(); err != nil { if err := ds.ensureDefaultDocument(); err != nil {
return fmt.Errorf("failed to ensure default document: %w", err) return fmt.Errorf("failed to ensure default document: %w", err)
} }
@@ -170,6 +183,10 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
doc.IsDeleted = isDeletedInt == 1 doc.IsDeleted = isDeletedInt == 1
} }
if isLockedInt, ok := row["is_locked"].(int64); ok {
doc.IsLocked = isLockedInt == 1
}
return doc, nil return doc, nil
} }
@@ -186,6 +203,7 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
IsDeleted: false, IsDeleted: false,
IsLocked: false,
} }
// 执行插入操作 // 执行插入操作
@@ -215,6 +233,61 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
return doc, nil return doc, nil
} }
// LockDocument 锁定文档,防止删除
func (ds *DocumentService) LockDocument(id int64) error {
ds.mu.Lock()
defer ds.mu.Unlock()
// 检查文档是否存在且未删除
doc, err := ds.GetDocumentByID(id)
if err != nil {
return fmt.Errorf("failed to get document: %w", err)
}
if doc == nil {
return fmt.Errorf("document not found: %d", id)
}
if doc.IsDeleted {
return fmt.Errorf("cannot lock deleted document: %d", id)
}
// 如果已经锁定,无需操作
if doc.IsLocked {
return nil
}
err = ds.databaseService.SQLite.Execute(sqlSetDocumentLocked, time.Now(), id)
if err != nil {
return fmt.Errorf("failed to lock document: %w", err)
}
return nil
}
// UnlockDocument 解锁文档
func (ds *DocumentService) UnlockDocument(id int64) error {
ds.mu.Lock()
defer ds.mu.Unlock()
// 检查文档是否存在
doc, err := ds.GetDocumentByID(id)
if err != nil {
return fmt.Errorf("failed to get document: %w", err)
}
if doc == nil {
return fmt.Errorf("document not found: %d", id)
}
// 如果未锁定,无需操作
if !doc.IsLocked {
return nil
}
err = ds.databaseService.SQLite.Execute(sqlSetDocumentUnlocked, time.Now(), id)
if err != nil {
return fmt.Errorf("failed to unlock document: %w", err)
}
return nil
}
// UpdateDocumentContent updates the content of a document // UpdateDocumentContent updates the content of a document
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error { func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
ds.mu.Lock() ds.mu.Lock()
@@ -249,7 +322,19 @@ func (ds *DocumentService) DeleteDocument(id int64) error {
return fmt.Errorf("cannot delete the default document") return fmt.Errorf("cannot delete the default document")
} }
err := ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id) // 检查文档是否锁定
doc, err := ds.GetDocumentByID(id)
if err != nil {
return fmt.Errorf("failed to get document: %w", err)
}
if doc == nil {
return fmt.Errorf("document not found: %d", id)
}
if doc.IsLocked {
return fmt.Errorf("cannot delete locked document: %d", id)
}
err = ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id)
if err != nil { if err != nil {
return fmt.Errorf("failed to mark document as deleted: %w", err) return fmt.Errorf("failed to mark document as deleted: %w", err)
} }
@@ -304,6 +389,10 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
} }
} }
if isLockedInt, ok := row["is_locked"].(int64); ok {
doc.IsLocked = isLockedInt == 1
}
documents = append(documents, doc) documents = append(documents, doc)
} }
@@ -346,6 +435,10 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error
} }
} }
if isLockedInt, ok := row["is_locked"].(int64); ok {
doc.IsLocked = isLockedInt == 1
}
documents = append(documents, doc) documents = append(documents, doc)
} }