Add configuration information file storage

This commit is contained in:
2025-04-27 18:04:08 +08:00
parent 946075f25d
commit 3ab209f899
20 changed files with 1106 additions and 58 deletions

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,51 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* A Time represents an instant in time with nanosecond precision.
*
* Programs using times should typically store and pass them as values,
* not pointers. That is, time variables and struct fields should be of
* type [time.Time], not *time.Time.
*
* A Time value can be used by multiple goroutines simultaneously except
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
* [Time.UnmarshalText] are not concurrency-safe.
*
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
* The [Time.Sub] method subtracts two instants, producing a [Duration].
* The [Time.Add] method adds a Time and a Duration, producing a Time.
*
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
* a simple way of detecting a time that has not been initialized explicitly.
*
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
* Time with a specific Location. Changing the Location of a Time value with
* these methods does not change the actual instant it represents, only the time
* zone in which to interpret it.
*
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
* but not the location name. They therefore lose information about Daylight Saving Time.
*
* In addition to the required “wall clock” reading, a Time may contain an optional
* reading of the current process's monotonic clock, to provide additional precision
* for comparison or subtraction.
* See the “Monotonic Clocks” section in the package documentation for details.
*
* Note that the Go == operator compares not just the time instant but also the
* Location and the monotonic clock reading. Therefore, Time values should not
* be used as map or database keys without first guaranteeing that the
* identical Location has been set for all values, which can be achieved
* through use of the UTC or Local method, and that the monotonic clock reading
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
* to t == u, since t.Equal uses the most accurate comparison available and
* correctly handles the case when only one of its arguments has a monotonic
* clock reading.
*/
export type Time = any;

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,219 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js";
/**
* AppConfig 应用配置
*/
export class AppConfig {
/**
* 编辑器配置
*/
"editor": EditorConfig;
/**
* 路径配置
*/
"paths": PathConfig;
/**
* 配置元数据
*/
"metadata": ConfigMetadata;
/** Creates a new AppConfig instance. */
constructor($$source: Partial<AppConfig> = {}) {
if (!("editor" in $$source)) {
this["editor"] = (new EditorConfig());
}
if (!("paths" in $$source)) {
this["paths"] = (new PathConfig());
}
if (!("metadata" in $$source)) {
this["metadata"] = (new ConfigMetadata());
}
Object.assign(this, $$source);
}
/**
* Creates a new AppConfig instance from a string or object.
*/
static createFrom($$source: any = {}): AppConfig {
const $$createField0_0 = $$createType0;
const $$createField1_0 = $$createType1;
const $$createField2_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("editor" in $$parsedSource) {
$$parsedSource["editor"] = $$createField0_0($$parsedSource["editor"]);
}
if ("paths" in $$parsedSource) {
$$parsedSource["paths"] = $$createField1_0($$parsedSource["paths"]);
}
if ("metadata" in $$parsedSource) {
$$parsedSource["metadata"] = $$createField2_0($$parsedSource["metadata"]);
}
return new AppConfig($$parsedSource as Partial<AppConfig>);
}
}
/**
* ConfigMetadata 配置元数据
*/
export class ConfigMetadata {
/**
* 配置版本
*/
"version": string;
/**
* 最后更新时间
*/
"lastUpdated": time$0.Time;
/** Creates a new ConfigMetadata instance. */
constructor($$source: Partial<ConfigMetadata> = {}) {
if (!("version" in $$source)) {
this["version"] = "";
}
if (!("lastUpdated" in $$source)) {
this["lastUpdated"] = null;
}
Object.assign(this, $$source);
}
/**
* Creates a new ConfigMetadata instance from a string or object.
*/
static createFrom($$source: any = {}): ConfigMetadata {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ConfigMetadata($$parsedSource as Partial<ConfigMetadata>);
}
}
/**
* EditorConfig 定义编辑器配置
*/
export class EditorConfig {
/**
* 字体大小
*/
"fontSize": number;
/**
* 文件保存的编码
*/
"encoding": string;
/**
* 是否启用Tab缩进
*/
"enableTabIndent": boolean;
/**
* Tab大小
*/
"tabSize": number;
/**
* Tab类型空格或Tab
*/
"tabType": TabType;
/** Creates a new EditorConfig instance. */
constructor($$source: Partial<EditorConfig> = {}) {
if (!("fontSize" in $$source)) {
this["fontSize"] = 0;
}
if (!("encoding" in $$source)) {
this["encoding"] = "";
}
if (!("enableTabIndent" in $$source)) {
this["enableTabIndent"] = false;
}
if (!("tabSize" in $$source)) {
this["tabSize"] = 0;
}
if (!("tabType" in $$source)) {
this["tabType"] = ("" as TabType);
}
Object.assign(this, $$source);
}
/**
* Creates a new EditorConfig instance from a string or object.
*/
static createFrom($$source: any = {}): EditorConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new EditorConfig($$parsedSource as Partial<EditorConfig>);
}
}
/**
* PathConfig 定义配置文件路径相关配置
*/
export class PathConfig {
/**
* 根目录
*/
"rootDir": string;
/**
* 配置文件路径
*/
"configPath": string;
/** Creates a new PathConfig instance. */
constructor($$source: Partial<PathConfig> = {}) {
if (!("rootDir" in $$source)) {
this["rootDir"] = "";
}
if (!("configPath" in $$source)) {
this["configPath"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new PathConfig instance from a string or object.
*/
static createFrom($$source: any = {}): PathConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new PathConfig($$parsedSource as Partial<PathConfig>);
}
}
/**
* TabType 定义了制表符类型
*/
export enum TabType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* TabTypeSpaces 使用空格作为制表符
*/
TabTypeSpaces = "spaces",
/**
* TabTypeTab 使用Tab作为制表符
*/
TabTypeTab = "tab",
};
// Private type creation functions
const $$createType0 = EditorConfig.createFrom;
const $$createType1 = PathConfig.createFrom;
const $$createType2 = ConfigMetadata.createFrom;

View File

@@ -0,0 +1,76 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* ConfigService 提供配置管理功能
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* GetAppConfig 获取应用配置
*/
export function GetAppConfig(): Promise<models$0.AppConfig | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3361428829) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetEditorConfig 获取编辑器配置
*/
export function GetEditorConfig(): Promise<models$0.EditorConfig> & { cancel(): void } {
let $resultPromise = $Call.ByID(3648153351) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetFullConfigPath 获取完整的配置文件路径
*/
export function GetFullConfigPath(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(38527092) as any;
return $resultPromise;
}
/**
* ResetToDefault 重置为默认配置
*/
export function ResetToDefault(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4057687351) as any;
return $resultPromise;
}
/**
* SaveAppConfig 保存应用配置
*/
export function SaveAppConfig(config: models$0.AppConfig | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2077587650, config) as any;
return $resultPromise;
}
/**
* UpdateEditorConfig 更新编辑器配置
*/
export function UpdateEditorConfig(editorConfig: models$0.EditorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1237949666, editorConfig) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.AppConfig.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = models$0.EditorConfig.createFrom;

View File

@@ -0,0 +1,51 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* FileService 提供原子化文件操作
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* DeleteFile 删除文件
*/
export function DeleteFile(filePath: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1771867857, filePath) as any;
return $resultPromise;
}
/**
* EnsureDir 确保目录存在,如不存在则创建
*/
export function EnsureDir(dirPath: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2291976369, dirPath) as any;
return $resultPromise;
}
/**
* FileExists 检查文件是否存在
*/
export function FileExists(filePath: string): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(4264173930, filePath) as any;
return $resultPromise;
}
/**
* LoadJSON 从文件加载JSON数据
*/
export function LoadJSON(filePath: string, target: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1385779418, filePath, target) as any;
return $resultPromise;
}
/**
* SaveJSON 原子化保存JSON数据到文件
*/
export function SaveJSON(filePath: string, data: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3646933935, filePath, data) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as ConfigService from "./configservice.js";
import * as FileService from "./fileservice.js";
export {
ConfigService,
FileService
};

View File

@@ -40,6 +40,7 @@
"@lezer/highlight": "^1.2.1",
"@primeuix/themes": "^1.0.3",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^13.1.0",
"codemirror": "^6.0.1",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
@@ -2156,6 +2157,12 @@
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.31.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz",
@@ -2600,6 +2607,44 @@
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "13.1.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.1.0.tgz",
"integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.1.0",
"@vueuse/shared": "13.1.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.1.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.1.0.tgz",
"integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "13.1.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.1.0.tgz",
"integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@wailsio/runtime": {
"version": "3.0.0-alpha.66",
"resolved": "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz",

View File

@@ -44,6 +44,7 @@
"@lezer/highlight": "^1.2.1",
"@primeuix/themes": "^1.0.3",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^13.1.0",
"codemirror": "^6.0.1",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",

View File

@@ -1,14 +1,21 @@
<script setup lang="ts">
import {onMounted} from 'vue';
import Editor from '@/editor/Editor.vue';
import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useConfigStore} from "@/stores/configStore";
const configStore = useConfigStore();
onMounted(async () => {
await configStore.loadConfigFromBackend();
})
</script>
<template>
<div class="app-container">
<div class="editor-wrapper">
<Editor />
<Editor/>
</div>
<Toolbar />
<Toolbar/>
</div>
</template>

View File

@@ -38,7 +38,7 @@ const configStore = useConfigStore();
</span>
</span>
<span class="encoding">{{ configStore.config.encoding }}</span>
<button class="settings-btn" @click="configStore.openSettings">
<button class="settings-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>

View File

@@ -7,10 +7,10 @@ import {useConfigStore} from '@/stores/configStore';
import {createBasicSetup} from './extensions/basicSetup';
import {
createStatsUpdateExtension,
getTabExtensions,
updateTabConfig,
createWheelZoomHandler,
updateStats
getTabExtensions,
updateStats,
updateTabConfig
} from './extensions';
const editorStore = useEditorStore();
@@ -19,7 +19,7 @@ const configStore = useConfigStore();
const props = defineProps({
initialDoc: {
type: String,
default: '// 在此处编写代码'
default: '// 在此处编写文本...'
}
});
@@ -34,14 +34,14 @@ const createEditor = () => {
// 获取Tab相关扩展
const tabExtensions = getTabExtensions(
configStore.config.tabSize,
configStore.config.enableTabIndent,
configStore.config.tabType
configStore.config.tabSize,
configStore.config.enableTabIndent,
configStore.config.tabType
);
// 创建统计信息更新扩展
const statsExtension = createStatsUpdateExtension(
editorStore.updateDocumentStats
editorStore.updateDocumentStats
);
// 组合所有扩展
@@ -75,18 +75,18 @@ const createEditor = () => {
// 创建滚轮事件处理器
const handleWheel = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
configStore.increaseFontSize,
configStore.decreaseFontSize
);
// 重新配置编辑器(仅在必要时)
const reconfigureTabSettings = () => {
if (!editorStore.editorView) return;
updateTabConfig(
editorStore.editorView as EditorView,
configStore.config.tabSize,
configStore.config.enableTabIndent,
configStore.config.tabType
editorStore.editorView as EditorView,
configStore.config.tabSize,
configStore.config.enableTabIndent,
configStore.config.tabType
);
};

View File

@@ -2,7 +2,7 @@ import {Compartment, Extension} from '@codemirror/state';
import {EditorView, keymap} from '@codemirror/view';
import {indentSelection} from '@codemirror/commands';
import {indentUnit} from '@codemirror/language';
import {TabType} from "@/types/config";
import {TabType} from '@/../bindings/voidraft/internal/models/models';
// Tab设置相关的compartment
export const tabSizeCompartment = new Compartment();

View File

@@ -1,6 +1,12 @@
import {defineStore} from 'pinia';
import {ref} from 'vue';
import {EditorConfig} from '@/types/config';
import {ref, watch} from 'vue';
import {useDebounceFn} from '@vueuse/core';
import {
GetEditorConfig,
ResetToDefault,
UpdateEditorConfig
} from '@/../bindings/voidraft/internal/services/configservice';
import {EditorConfig, TabType} from '@/../bindings/voidraft/internal/models/models';
// 字体大小范围
const MIN_FONT_SIZE = 12;
@@ -14,13 +20,43 @@ const MAX_TAB_SIZE = 8;
export const useConfigStore = defineStore('config', () => {
// 配置状态
const config = ref<EditorConfig>({
const config = ref<EditorConfig>(new EditorConfig({
fontSize: DEFAULT_FONT_SIZE,
encoding: 'UTF-8',
enableTabIndent: true,
tabSize: DEFAULT_TAB_SIZE,
tabType: 'spaces'
});
tabType: TabType.TabTypeSpaces
}));
// 配置是否已从后端加载
const configLoaded = ref(false);
// 从后端加载配置
async function loadConfigFromBackend() {
try {
const editorConfig = await GetEditorConfig();
config.value = editorConfig;
configLoaded.value = true;
} catch (error) {
console.error('Failed to load configuration:', error);
}
}
// 使用防抖保存配置到后端
const saveConfigToBackend = useDebounceFn(async () => {
try {
await UpdateEditorConfig(config.value);
} catch (error) {
console.error('Failed to save configuration:', error);
}
}, 500); // 500ms防抖
// 监听配置变化,自动保存到后端
watch(() => config.value, async () => {
if (configLoaded.value) {
await saveConfigToBackend();
}
}, {deep: true});
// 字体缩放
function increaseFontSize() {
@@ -67,18 +103,20 @@ export const useConfigStore = defineStore('config', () => {
// 切换Tab类型空格或制表符
function toggleTabType() {
config.value.tabType = config.value.tabType === 'spaces' ? 'tab' : 'spaces';
config.value.tabType = config.value.tabType === TabType.TabTypeSpaces
? TabType.TabTypeTab
: TabType.TabTypeSpaces;
}
// 设置按钮操作
function openSettings() {
console.log('打开设置面板');
// 此处可以实现设置面板的逻辑
// 重置为默认配置
async function resetToDefaults() {
await ResetToDefault();
await loadConfigFromBackend();
}
return {
// 状态
config,
configLoaded,
// 常量
MIN_FONT_SIZE,
@@ -88,19 +126,16 @@ export const useConfigStore = defineStore('config', () => {
MAX_TAB_SIZE,
// 方法
loadConfigFromBackend,
saveConfigToBackend,
setEncoding,
openSettings,
increaseFontSize,
decreaseFontSize,
resetFontSize,
toggleTabIndent,
increaseTabSize,
decreaseTabSize,
toggleTabType
toggleTabType,
resetToDefaults
};
}, {
persist: {
key: 'editor-config',
storage: localStorage
}
});

View File

@@ -1,11 +0,0 @@
// Tab类型
export type TabType = 'spaces' | 'tab';
// 编辑器配置接口
export interface EditorConfig {
fontSize: number;
encoding: string;
enableTabIndent: boolean;
tabSize: number;
tabType: TabType;
}

64
internal/models/config.go Normal file
View File

@@ -0,0 +1,64 @@
package models
import (
"time"
)
// TabType 定义了制表符类型
type TabType string
const (
// TabTypeSpaces 使用空格作为制表符
TabTypeSpaces TabType = "spaces"
// TabTypeTab 使用Tab作为制表符
TabTypeTab TabType = "tab"
)
// EditorConfig 定义编辑器配置
type EditorConfig struct {
FontSize int `json:"fontSize"` // 字体大小
Encoding string `json:"encoding"` // 文件保存的编码
EnableTabIndent bool `json:"enableTabIndent"` // 是否启用Tab缩进
TabSize int `json:"tabSize"` // Tab大小
TabType TabType `json:"tabType"` // Tab类型空格或Tab
}
// PathConfig 定义配置文件路径相关配置
type PathConfig struct {
RootDir string `json:"rootDir"` // 根目录
ConfigPath string `json:"configPath"` // 配置文件路径
}
// AppConfig 应用配置
type AppConfig struct {
Editor EditorConfig `json:"editor"` // 编辑器配置
Paths PathConfig `json:"paths"` // 路径配置
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
}
// ConfigMetadata 配置元数据
type ConfigMetadata struct {
Version string `json:"version"` // 配置版本
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
}
// NewDefaultAppConfig 创建默认应用配置
func NewDefaultAppConfig() *AppConfig {
return &AppConfig{
Editor: EditorConfig{
FontSize: 13,
Encoding: "UTF-8",
EnableTabIndent: true,
TabSize: 4,
TabType: TabTypeSpaces,
},
Paths: PathConfig{
RootDir: ".voidraft",
ConfigPath: "config/config.json",
},
Metadata: ConfigMetadata{
Version: "1.0.0",
LastUpdated: time.Now(),
},
}
}

View File

@@ -0,0 +1,298 @@
package services
import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
"voidraft/internal/models"
)
// ConfigService 提供配置管理功能
type ConfigService struct {
fileService *FileService
configPath string
rootDir string
homePath string
mutex sync.RWMutex
config *models.AppConfig // 缓存最新配置
}
// NewConfigService 创建新的配置服务实例
func NewConfigService(fileService *FileService) *ConfigService {
// 获取用户主目录
homePath, err := os.UserHomeDir()
if err != nil {
log.Printf("Failed to get user home directory: %v", err)
homePath = "."
}
log.Printf("User home directory: %s", homePath)
// 创建默认配置
defaultConfig := models.NewDefaultAppConfig()
// 构造服务实例
service := &ConfigService{
fileService: fileService,
rootDir: defaultConfig.Paths.RootDir,
configPath: defaultConfig.Paths.ConfigPath,
homePath: homePath,
config: defaultConfig, // 初始化缓存配置
}
// 初始化配置目录和文件(非锁定方式)
service.initializeConfig()
return service
}
// initializeConfig 初始化配置目录和文件,避免死锁
func (cs *ConfigService) initializeConfig() {
// 确保配置目录存在
dirPath := filepath.Join(cs.homePath, cs.rootDir)
log.Printf("Creating config directory: %s", dirPath)
// 确保主目录存在
if err := cs.fileService.EnsureDir(dirPath); err != nil {
log.Printf("Failed to create config directory: %v", err)
return
}
// 确保配置文件所在目录存在
configDir := filepath.Dir(cs.GetFullConfigPath())
if configDir != dirPath {
if err := cs.fileService.EnsureDir(configDir); err != nil {
log.Printf("Failed to create config file directory: %v", err)
return
}
}
// 检查配置文件是否存在
configFilePath := cs.GetFullConfigPath()
log.Printf("Config file path: %s", configFilePath)
if !cs.fileService.FileExists(configFilePath) {
log.Printf("Config file not found, creating default config")
// 创建默认配置文件
defaultConfig := models.NewDefaultAppConfig()
if err := cs.saveAppConfigInitial(defaultConfig); err != nil {
log.Printf("Failed to save default config: %v", err)
}
} else {
// 加载现有配置
log.Printf("Loading existing config file")
existingConfig, err := cs.loadAppConfigInitial()
if err != nil {
log.Printf("Failed to load existing config, using default: %v", err)
} else {
cs.config = existingConfig
}
}
}
// saveAppConfigInitial 初始化时保存配置,不使用互斥锁
func (cs *ConfigService) saveAppConfigInitial(config *models.AppConfig) error {
// 更新配置元数据
config.Metadata.LastUpdated = time.Now()
// 保存到文件
configPath := cs.GetFullConfigPath()
log.Printf("saveAppConfigInitial: Saving to %s", configPath)
if err := cs.fileService.SaveJSON(configPath, config); err != nil {
return fmt.Errorf("failed to save initial config file: %w", err)
}
// 更新内存中的配置
cs.config = config
return nil
}
// loadAppConfigInitial 初始化时加载配置,不使用互斥锁
func (cs *ConfigService) loadAppConfigInitial() (*models.AppConfig, error) {
config := &models.AppConfig{}
configPath := cs.GetFullConfigPath()
if err := cs.fileService.LoadJSON(configPath, config); err != nil {
return nil, fmt.Errorf("failed to load initial config file: %w", err)
}
return config, nil
}
// initConfigDir 确保目录存在,如不存在则创建
func (cs *ConfigService) initConfigDir() error {
configDir := filepath.Join(cs.homePath, cs.rootDir)
return cs.fileService.EnsureDir(configDir)
}
// GetFullConfigPath 获取完整的配置文件路径
func (cs *ConfigService) GetFullConfigPath() string {
return filepath.Join(cs.homePath, cs.rootDir, cs.configPath)
}
// GetAppConfig 获取应用配置
func (cs *ConfigService) GetAppConfig() (*models.AppConfig, error) {
cs.mutex.RLock()
defer cs.mutex.RUnlock()
// 返回内存中的配置副本
if cs.config != nil {
return cs.config, nil
}
// 从文件加载
config := &models.AppConfig{}
configPath := cs.GetFullConfigPath()
log.Printf("GetAppConfig: Loading from %s", configPath)
// 如果配置文件存在,则加载
if cs.fileService.FileExists(configPath) {
if err := cs.fileService.LoadJSON(configPath, config); err != nil {
log.Printf("GetAppConfig: Failed to load config: %v", err)
return nil, fmt.Errorf("failed to load config file: %w", err)
}
log.Printf("GetAppConfig: Successfully loaded config")
// 更新内存中的配置
cs.config = config
} else {
// 文件不存在,使用默认配置
log.Printf("GetAppConfig: Config file not found, using default")
config = models.NewDefaultAppConfig()
// 保存默认配置到文件
if err := cs.SaveAppConfig(config); err != nil {
log.Printf("GetAppConfig: Failed to save default config: %v", err)
return nil, fmt.Errorf("failed to save default config: %w", err)
}
}
return config, nil
}
// SaveAppConfig 保存应用配置
func (cs *ConfigService) SaveAppConfig(config *models.AppConfig) error {
cs.mutex.Lock()
defer cs.mutex.Unlock()
// 更新配置元数据
config.Metadata.LastUpdated = time.Now()
// 保存到文件
configPath := cs.GetFullConfigPath()
log.Printf("SaveAppConfig: Saving to %s", configPath)
if err := cs.fileService.SaveJSON(configPath, config); err != nil {
log.Printf("SaveAppConfig: Failed to save config: %v", err)
return fmt.Errorf("failed to save config file: %w", err)
}
// 更新内存中的配置
cs.config = config
log.Printf("SaveAppConfig: Successfully saved config")
return nil
}
// UpdateEditorConfig 更新编辑器配置
func (cs *ConfigService) UpdateEditorConfig(editorConfig models.EditorConfig) error {
cs.mutex.Lock()
defer cs.mutex.Unlock()
// 如果内存中已有配置,直接更新
if cs.config != nil {
log.Printf("UpdateEditorConfig: Updating in-memory editor config: %+v", editorConfig)
cs.config.Editor = editorConfig
// 保存到文件
configPath := cs.GetFullConfigPath()
if err := cs.fileService.SaveJSON(configPath, cs.config); err != nil {
log.Printf("UpdateEditorConfig: Failed to save config: %v", err)
return fmt.Errorf("failed to save config file: %w", err)
}
log.Printf("UpdateEditorConfig: Successfully saved updated config")
return nil
}
// 没有内存中的配置,需要先加载
config, err := cs.loadAppConfigInitial()
if err != nil {
log.Printf("UpdateEditorConfig: Failed to load config: %v", err)
// 使用默认配置
config = models.NewDefaultAppConfig()
}
// 更新编辑器配置
config.Editor = editorConfig
// 更新配置元数据
config.Metadata.LastUpdated = time.Now()
// 保存到文件
configPath := cs.GetFullConfigPath()
if err := cs.fileService.SaveJSON(configPath, config); err != nil {
log.Printf("UpdateEditorConfig: Failed to save config: %v", err)
return fmt.Errorf("failed to save config file: %w", err)
}
// 更新内存中的配置
cs.config = config
log.Printf("UpdateEditorConfig: Successfully saved config with updated editor settings")
return nil
}
// GetEditorConfig 获取编辑器配置
func (cs *ConfigService) GetEditorConfig() (models.EditorConfig, error) {
cs.mutex.RLock()
defer cs.mutex.RUnlock()
// 如果内存中已有配置,直接返回
if cs.config != nil {
return cs.config.Editor, nil
}
// 否则从文件加载
config, err := cs.loadAppConfigInitial()
if err != nil {
log.Printf("GetEditorConfig: Failed to load config: %v", err)
// 使用默认配置
defaultConfig := models.NewDefaultAppConfig()
cs.config = defaultConfig
return defaultConfig.Editor, nil
}
// 更新内存中的配置
cs.config = config
log.Printf("GetEditorConfig: Retrieved editor config: %+v", config.Editor)
return config.Editor, nil
}
// ResetToDefault 重置为默认配置
func (cs *ConfigService) ResetToDefault() error {
cs.mutex.Lock()
defer cs.mutex.Unlock()
// 创建默认配置
defaultConfig := models.NewDefaultAppConfig()
log.Printf("ResetToDefault: Resetting to default config")
// 保存到文件
configPath := cs.GetFullConfigPath()
if err := cs.fileService.SaveJSON(configPath, defaultConfig); err != nil {
log.Printf("ResetToDefault: Failed to save default config: %v", err)
return fmt.Errorf("failed to save default config: %w", err)
}
// 更新内存中的配置
cs.config = defaultConfig
log.Printf("ResetToDefault: Successfully reset to default config")
return nil
}

View File

@@ -0,0 +1,146 @@
package services
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
)
// FileService 提供原子化文件操作
type FileService struct {
mutex sync.Mutex
}
// NewFileService 创建新的文件服务实例
func NewFileService() *FileService {
return &FileService{}
}
// EnsureDir 确保目录存在,如不存在则创建
func (fs *FileService) EnsureDir(dirPath string) error {
fs.mutex.Lock()
defer fs.mutex.Unlock()
return fs.ensureDirNoLock(dirPath)
}
// ensureDirNoLock 无锁版本的EnsureDir仅供内部使用
func (fs *FileService) ensureDirNoLock(dirPath string) error {
log.Printf("EnsureDir: Checking directory: %s", dirPath)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
log.Printf("EnsureDir: Directory does not exist, creating: %s", dirPath)
err := os.MkdirAll(dirPath, 0755)
if err != nil {
log.Printf("EnsureDir: Failed to create directory: %v", err)
return err
}
log.Printf("EnsureDir: Directory created successfully: %s", dirPath)
} else {
log.Printf("EnsureDir: Directory already exists: %s", dirPath)
}
return nil
}
// SaveJSON 原子化保存JSON数据到文件
func (fs *FileService) SaveJSON(filePath string, data interface{}) error {
fs.mutex.Lock()
defer fs.mutex.Unlock()
log.Printf("SaveJSON: Saving to file: %s", filePath)
// 确保目录存在 - 使用无锁版本以避免死锁
dir := filepath.Dir(filePath)
if err := fs.ensureDirNoLock(dir); err != nil {
log.Printf("SaveJSON: Failed to create directory: %v", err)
return fmt.Errorf("failed to create directory: %w", err)
}
// 将数据编码为JSON
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Printf("SaveJSON: Failed to encode JSON: %v", err)
return fmt.Errorf("failed to encode JSON: %w", err)
}
// 先写入临时文件
tempFile := filePath + ".tmp"
log.Printf("SaveJSON: Writing to temporary file: %s", tempFile)
if err := os.WriteFile(tempFile, jsonData, 0644); err != nil {
log.Printf("SaveJSON: Failed to write temporary file: %v", err)
return fmt.Errorf("failed to write temporary file: %w", err)
}
// 原子替换原文件
log.Printf("SaveJSON: Replacing original file with temporary file")
if err := os.Rename(tempFile, filePath); err != nil {
os.Remove(tempFile) // 清理临时文件
log.Printf("SaveJSON: Failed to replace file: %v", err)
return fmt.Errorf("failed to replace file: %w", err)
}
log.Printf("SaveJSON: File saved successfully: %s", filePath)
return nil
}
// LoadJSON 从文件加载JSON数据
func (fs *FileService) LoadJSON(filePath string, target interface{}) error {
fs.mutex.Lock()
defer fs.mutex.Unlock()
log.Printf("LoadJSON: Loading from file: %s", filePath)
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
log.Printf("LoadJSON: File does not exist: %s", filePath)
return fmt.Errorf("file does not exist: %w", err)
}
// 读取文件内容
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("LoadJSON: Failed to read file: %v", err)
return fmt.Errorf("failed to read file: %w", err)
}
// 解析JSON数据
if err := json.Unmarshal(data, target); err != nil {
log.Printf("LoadJSON: Failed to parse JSON: %v", err)
return fmt.Errorf("failed to parse JSON: %w", err)
}
log.Printf("LoadJSON: File loaded successfully: %s", filePath)
return nil
}
// FileExists 检查文件是否存在
func (fs *FileService) FileExists(filePath string) bool {
_, err := os.Stat(filePath)
exists := !os.IsNotExist(err)
log.Printf("FileExists: Checking if file exists: %s, exists: %v", filePath, exists)
return exists
}
// DeleteFile 删除文件
func (fs *FileService) DeleteFile(filePath string) error {
fs.mutex.Lock()
defer fs.mutex.Unlock()
log.Printf("DeleteFile: Deleting file: %s", filePath)
if !fs.FileExists(filePath) {
log.Printf("DeleteFile: File does not exist, nothing to delete: %s", filePath)
return nil // 文件不存在视为删除成功
}
err := os.Remove(filePath)
if err != nil {
log.Printf("DeleteFile: Failed to delete file: %v", err)
} else {
log.Printf("DeleteFile: File deleted successfully: %s", filePath)
}
return err
}

View File

@@ -0,0 +1,43 @@
package services
import (
"github.com/wailsapp/wails/v3/pkg/application"
)
// ServiceManager 服务管理器,负责协调各个服务
type ServiceManager struct {
fileService *FileService
configService *ConfigService
}
// NewServiceManager 创建新的服务管理器实例
func NewServiceManager() *ServiceManager {
// 初始化文件服务
fileService := NewFileService()
// 初始化配置服务
configService := NewConfigService(fileService)
return &ServiceManager{
fileService: fileService,
configService: configService,
}
}
// GetServices 获取所有wails服务列表
func (sm *ServiceManager) GetServices() []application.Service {
return []application.Service{
application.NewService(sm.fileService),
application.NewService(sm.configService),
}
}
// GetFileService 获取文件服务实例
func (sm *ServiceManager) GetFileService() *FileService {
return sm.fileService
}
// GetConfigService 获取配置服务实例
func (sm *ServiceManager) GetConfigService() *ConfigService {
return sm.configService
}

10
main.go
View File

@@ -5,6 +5,7 @@ import (
_ "embed"
"log"
"time"
"voidraft/internal/services"
"github.com/wailsapp/wails/v3/pkg/application"
)
@@ -21,16 +22,17 @@ var assets embed.FS
// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and
// logs any error that might occur.
func main() {
serviceManager := services.NewServiceManager()
// Create a new Wails application by providing the necessary options.
// Variables 'Name' and 'Description' are for application metadata.
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances.
// 'Mac' options tailor the application when running an macOS.
log.Println("Creating Wails application...")
app := application.New(application.Options{
Name: "voidraft",
Description: "voidraft",
Services: []application.Service{},
Services: serviceManager.GetServices(),
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
@@ -44,6 +46,7 @@ func main() {
// 'Mac' options tailor the window when running on macOS.
// 'BackgroundColour' is the background colour of the window.
// 'URL' is the URL that will be loaded into the webview.
log.Println("Creating main window...")
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "voidraft",
Width: 700,
@@ -68,10 +71,13 @@ func main() {
}()
// Run the application. This blocks until the application has been exited.
log.Println("Starting application event loop...")
err := app.Run()
// If an error occurred while running the application, log it and exit.
if err != nil {
log.Fatal(err)
}
log.Println("Application exiting...")
}