✨ Add self-updating service
This commit is contained in:
@@ -471,6 +471,84 @@ export class GeneralConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GiteaConfig Gitea配置
|
||||
*/
|
||||
export class GiteaConfig {
|
||||
/**
|
||||
* Gitea服务器URL
|
||||
*/
|
||||
"baseURL": string;
|
||||
|
||||
/**
|
||||
* 仓库所有者
|
||||
*/
|
||||
"owner": string;
|
||||
|
||||
/**
|
||||
* 仓库名称
|
||||
*/
|
||||
"repo": string;
|
||||
|
||||
/** Creates a new GiteaConfig instance. */
|
||||
constructor($$source: Partial<GiteaConfig> = {}) {
|
||||
if (!("baseURL" in $$source)) {
|
||||
this["baseURL"] = "";
|
||||
}
|
||||
if (!("owner" in $$source)) {
|
||||
this["owner"] = "";
|
||||
}
|
||||
if (!("repo" in $$source)) {
|
||||
this["repo"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GiteaConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): GiteaConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new GiteaConfig($$parsedSource as Partial<GiteaConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GithubConfig GitHub配置
|
||||
*/
|
||||
export class GithubConfig {
|
||||
/**
|
||||
* 仓库所有者
|
||||
*/
|
||||
"owner": string;
|
||||
|
||||
/**
|
||||
* 仓库名称
|
||||
*/
|
||||
"repo": string;
|
||||
|
||||
/** Creates a new GithubConfig instance. */
|
||||
constructor($$source: Partial<GithubConfig> = {}) {
|
||||
if (!("owner" in $$source)) {
|
||||
this["owner"] = "";
|
||||
}
|
||||
if (!("repo" in $$source)) {
|
||||
this["repo"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GithubConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): GithubConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new GithubConfig($$parsedSource as Partial<GithubConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HotkeyCombo 热键组合定义
|
||||
*/
|
||||
@@ -1023,6 +1101,26 @@ export enum TabType {
|
||||
TabTypeTab = "tab",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateSourceType 更新源类型
|
||||
*/
|
||||
export enum UpdateSourceType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* UpdateSourceGithub GitHub更新源
|
||||
*/
|
||||
UpdateSourceGithub = "github",
|
||||
|
||||
/**
|
||||
* UpdateSourceGitea Gitea更新源
|
||||
*/
|
||||
UpdateSourceGitea = "gitea",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdatesConfig 更新设置配置
|
||||
*/
|
||||
@@ -1037,6 +1135,36 @@ export class UpdatesConfig {
|
||||
*/
|
||||
"autoUpdate": boolean;
|
||||
|
||||
/**
|
||||
* 主要更新源
|
||||
*/
|
||||
"primarySource": UpdateSourceType;
|
||||
|
||||
/**
|
||||
* 备用更新源
|
||||
*/
|
||||
"backupSource": UpdateSourceType;
|
||||
|
||||
/**
|
||||
* 更新前是否备份
|
||||
*/
|
||||
"backupBeforeUpdate": boolean;
|
||||
|
||||
/**
|
||||
* 更新超时时间(秒)
|
||||
*/
|
||||
"updateTimeout": number;
|
||||
|
||||
/**
|
||||
* GitHub配置
|
||||
*/
|
||||
"github": GithubConfig;
|
||||
|
||||
/**
|
||||
* Gitea配置
|
||||
*/
|
||||
"gitea": GiteaConfig;
|
||||
|
||||
/** Creates a new UpdatesConfig instance. */
|
||||
constructor($$source: Partial<UpdatesConfig> = {}) {
|
||||
if (!("version" in $$source)) {
|
||||
@@ -1045,6 +1173,24 @@ export class UpdatesConfig {
|
||||
if (!("autoUpdate" in $$source)) {
|
||||
this["autoUpdate"] = false;
|
||||
}
|
||||
if (!("primarySource" in $$source)) {
|
||||
this["primarySource"] = ("" as UpdateSourceType);
|
||||
}
|
||||
if (!("backupSource" in $$source)) {
|
||||
this["backupSource"] = ("" as UpdateSourceType);
|
||||
}
|
||||
if (!("backupBeforeUpdate" in $$source)) {
|
||||
this["backupBeforeUpdate"] = false;
|
||||
}
|
||||
if (!("updateTimeout" in $$source)) {
|
||||
this["updateTimeout"] = 0;
|
||||
}
|
||||
if (!("github" in $$source)) {
|
||||
this["github"] = (new GithubConfig());
|
||||
}
|
||||
if (!("gitea" in $$source)) {
|
||||
this["gitea"] = (new GiteaConfig());
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -1053,7 +1199,15 @@ export class UpdatesConfig {
|
||||
* Creates a new UpdatesConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): UpdatesConfig {
|
||||
const $$createField6_0 = $$createType11;
|
||||
const $$createField7_0 = $$createType12;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("github" in $$parsedSource) {
|
||||
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
|
||||
}
|
||||
if ("gitea" in $$parsedSource) {
|
||||
$$parsedSource["gitea"] = $$createField7_0($$parsedSource["gitea"]);
|
||||
}
|
||||
return new UpdatesConfig($$parsedSource as Partial<UpdatesConfig>);
|
||||
}
|
||||
}
|
||||
@@ -1075,3 +1229,5 @@ const $$createType7 = HotkeyCombo.createFrom;
|
||||
const $$createType8 = KeyBinding.createFrom;
|
||||
const $$createType9 = $Create.Array($$createType8);
|
||||
const $$createType10 = KeyBindingMetadata.createFrom;
|
||||
const $$createType11 = GithubConfig.createFrom;
|
||||
const $$createType12 = GiteaConfig.createFrom;
|
||||
|
@@ -8,10 +8,10 @@ import * as ExtensionService from "./extensionservice.js";
|
||||
import * as HotkeyService from "./hotkeyservice.js";
|
||||
import * as KeyBindingService from "./keybindingservice.js";
|
||||
import * as MigrationService from "./migrationservice.js";
|
||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||
import * as StartupService from "./startupservice.js";
|
||||
import * as SystemService from "./systemservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as UpdateService from "./updateservice.js";
|
||||
export {
|
||||
ConfigService,
|
||||
DialogService,
|
||||
@@ -20,10 +20,10 @@ export {
|
||||
HotkeyService,
|
||||
KeyBindingService,
|
||||
MigrationService,
|
||||
SelfUpdateService,
|
||||
StartupService,
|
||||
SystemService,
|
||||
TrayService,
|
||||
UpdateService
|
||||
TrayService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
|
@@ -116,9 +116,9 @@ export enum MigrationStatus {
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateCheckResult 更新检查结果
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
export class UpdateCheckResult {
|
||||
export class SelfUpdateResult {
|
||||
/**
|
||||
* 是否有更新
|
||||
*/
|
||||
@@ -127,57 +127,73 @@ export class UpdateCheckResult {
|
||||
/**
|
||||
* 当前版本
|
||||
*/
|
||||
"currentVer": string;
|
||||
"currentVersion": string;
|
||||
|
||||
/**
|
||||
* 最新版本
|
||||
*/
|
||||
"latestVer": string;
|
||||
"latestVersion": string;
|
||||
|
||||
/**
|
||||
* 是否已应用更新
|
||||
*/
|
||||
"updateApplied": boolean;
|
||||
|
||||
/**
|
||||
* 下载链接
|
||||
*/
|
||||
"assetURL": string;
|
||||
|
||||
/**
|
||||
* 发布说明
|
||||
*/
|
||||
"releaseNotes": string;
|
||||
|
||||
/**
|
||||
* 发布页面URL
|
||||
*/
|
||||
"releaseURL": string;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
"error": string;
|
||||
|
||||
/** Creates a new UpdateCheckResult instance. */
|
||||
constructor($$source: Partial<UpdateCheckResult> = {}) {
|
||||
/**
|
||||
* 更新源(github/gitea)
|
||||
*/
|
||||
"source": string;
|
||||
|
||||
/** Creates a new SelfUpdateResult instance. */
|
||||
constructor($$source: Partial<SelfUpdateResult> = {}) {
|
||||
if (!("hasUpdate" in $$source)) {
|
||||
this["hasUpdate"] = false;
|
||||
}
|
||||
if (!("currentVer" in $$source)) {
|
||||
this["currentVer"] = "";
|
||||
if (!("currentVersion" in $$source)) {
|
||||
this["currentVersion"] = "";
|
||||
}
|
||||
if (!("latestVer" in $$source)) {
|
||||
this["latestVer"] = "";
|
||||
if (!("latestVersion" in $$source)) {
|
||||
this["latestVersion"] = "";
|
||||
}
|
||||
if (!("updateApplied" in $$source)) {
|
||||
this["updateApplied"] = false;
|
||||
}
|
||||
if (!("assetURL" in $$source)) {
|
||||
this["assetURL"] = "";
|
||||
}
|
||||
if (!("releaseNotes" in $$source)) {
|
||||
this["releaseNotes"] = "";
|
||||
}
|
||||
if (!("releaseURL" in $$source)) {
|
||||
this["releaseURL"] = "";
|
||||
}
|
||||
if (!("error" in $$source)) {
|
||||
this["error"] = "";
|
||||
}
|
||||
if (!("source" in $$source)) {
|
||||
this["source"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new UpdateCheckResult instance from a string or object.
|
||||
* Creates a new SelfUpdateResult instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): UpdateCheckResult {
|
||||
static createFrom($$source: any = {}): SelfUpdateResult {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new UpdateCheckResult($$parsedSource as Partial<UpdateCheckResult>);
|
||||
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,51 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* SelfUpdateService 自我更新服务
|
||||
* @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 from "./models.js";
|
||||
|
||||
/**
|
||||
* ApplyUpdate 应用更新
|
||||
*/
|
||||
export function ApplyUpdate(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2009328394) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckForUpdates 检查更新
|
||||
*/
|
||||
export function CheckForUpdates(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(438757208) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RestartApplication 重启应用程序
|
||||
*/
|
||||
export function RestartApplication(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3341481538) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.SelfUpdateResult.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
@@ -1,30 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* UpdateService 更新服务
|
||||
* @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 from "./models.js";
|
||||
|
||||
/**
|
||||
* CheckForUpdates 检查更新
|
||||
*/
|
||||
export function CheckForUpdates(): Promise<$models.UpdateCheckResult> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3024322786) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.UpdateCheckResult.createFrom;
|
55
frontend/package-lock.json
generated
55
frontend/package-lock.json
generated
@@ -49,6 +49,7 @@
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"prettier": "^3.5.3",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.6",
|
||||
@@ -58,6 +59,7 @@
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@lezer/generator": "^1.7.3",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@wailsio/runtime": "latest",
|
||||
"eslint": "^9.29.0",
|
||||
@@ -2099,6 +2101,13 @@
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/remarkable": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/remarkable/-/remarkable-2.0.8.tgz",
|
||||
"integrity": "sha512-eKXqPZfpQl1kOADjdKchHrp2gwn9qMnGXhH/AtZe0UrklzhGJkawJo/Y/D0AlWcdWoWamFNIum8+/nkAISQVGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.34.1",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||
@@ -2685,6 +2694,15 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/autolinker": {
|
||||
"version": "3.16.2",
|
||||
"resolved": "https://registry.npmmirror.com/autolinker/-/autolinker-3.16.2.tgz",
|
||||
"integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -4337,6 +4355,31 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/remarkable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/remarkable/-/remarkable-2.0.1.tgz",
|
||||
"integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.10",
|
||||
"autolinker": "^3.11.0"
|
||||
},
|
||||
"bin": {
|
||||
"remarkable": "bin/remarkable.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/remarkable/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -4510,6 +4553,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz",
|
||||
@@ -4655,6 +4704,12 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@@ -53,6 +53,7 @@
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"prettier": "^3.5.3",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.6",
|
||||
@@ -62,6 +63,7 @@
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@lezer/generator": "^1.7.3",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@wailsio/runtime": "latest",
|
||||
"eslint": "^9.29.0",
|
||||
|
@@ -1,18 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
||||
import { SystemService } from '@/../bindings/voidraft/internal/services';
|
||||
import type { MemoryStats } from '@/../bindings/voidraft/internal/services';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
const { t } = useI18n();
|
||||
const themeStore = useThemeStore();
|
||||
const memoryStats = ref<MemoryStats | null>(null);
|
||||
const formattedMemory = ref('');
|
||||
const isLoading = ref(true);
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 存储历史数据点 (最近60个数据点,每3秒一个点,总共3分钟历史)
|
||||
// 存储历史数据点 (最近60个数据点)
|
||||
const historyData = ref<number[]>([]);
|
||||
const maxDataPoints = 60;
|
||||
|
||||
// 动态最大内存值(MB),初始为200MB,会根据实际使用动态调整
|
||||
const maxMemoryMB = ref(200);
|
||||
|
||||
// 使用themeStore获取当前主题
|
||||
const isDarkTheme = computed(() => {
|
||||
const theme = themeStore.currentTheme;
|
||||
if (theme === SystemThemeType.SystemThemeAuto) {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return theme === SystemThemeType.SystemThemeDark;
|
||||
});
|
||||
|
||||
// 监听主题变化,重新绘制图表
|
||||
watch(() => themeStore.currentTheme, () => {
|
||||
nextTick(() => drawChart());
|
||||
});
|
||||
|
||||
// 静默错误处理包装器
|
||||
const withSilentErrorHandling = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
@@ -47,8 +69,14 @@ const fetchMemoryStats = async () => {
|
||||
formattedMemory.value = `${heapMB.toFixed(0)}M`;
|
||||
}
|
||||
|
||||
// 添加新数据点到历史记录
|
||||
const memoryUsagePercent = Math.min((stats.heapInUse / (100 * 1024 * 1024)) * 100, 100);
|
||||
// 自动调整最大内存值,确保图表能够显示更大范围
|
||||
if (heapMB > maxMemoryMB.value * 0.8) {
|
||||
// 如果内存使用超过当前最大值的80%,则将最大值调整为当前使用值的2倍
|
||||
maxMemoryMB.value = Math.ceil(heapMB * 2);
|
||||
}
|
||||
|
||||
// 添加新数据点到历史记录 - 使用动态最大值计算百分比
|
||||
const memoryUsagePercent = Math.min((heapMB / maxMemoryMB.value) * 100, 100);
|
||||
historyData.value.push(memoryUsagePercent);
|
||||
|
||||
// 保持最大数据点数量
|
||||
@@ -62,7 +90,7 @@ const fetchMemoryStats = async () => {
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// 绘制实时曲线图
|
||||
// 绘制实时曲线图 - 简化版
|
||||
const drawChart = () => {
|
||||
if (!canvasRef.value || historyData.value.length === 0) return;
|
||||
|
||||
@@ -82,11 +110,16 @@ const drawChart = () => {
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 绘制背景网格 - 朦胧的网格,从上到下逐渐清晰
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
const y = (height / 6) * i;
|
||||
const opacity = 0.01 + (i / 6) * 0.03; // 从上到下逐渐清晰
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
|
||||
// 根据主题选择合适的颜色 - 更柔和的颜色
|
||||
const gridColor = isDarkTheme.value ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)';
|
||||
const lineColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)';
|
||||
const fillColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)';
|
||||
const pointColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)';
|
||||
|
||||
// 绘制背景网格 - 更加柔和
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = (height / 4) * i;
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
@@ -95,9 +128,9 @@ const drawChart = () => {
|
||||
}
|
||||
|
||||
// 垂直网格线
|
||||
for (let i = 0; i <= 8; i++) {
|
||||
const x = (width / 8) * i;
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.02)';
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
const x = (width / 6) * i;
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
@@ -112,13 +145,7 @@ const drawChart = () => {
|
||||
const stepX = width / (maxDataPoints - 1);
|
||||
const startX = width - (dataLength - 1) * stepX;
|
||||
|
||||
// 绘制填充区域 - 从上朦胧到下清晰的渐变
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(74, 158, 255, 0.1)'); // 顶部很淡
|
||||
gradient.addColorStop(0.3, 'rgba(74, 158, 255, 0.15)');
|
||||
gradient.addColorStop(0.7, 'rgba(74, 158, 255, 0.25)');
|
||||
gradient.addColorStop(1, 'rgba(74, 158, 255, 0.4)'); // 底部较浓
|
||||
|
||||
// 绘制填充区域 - 更柔和的填充
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, height);
|
||||
|
||||
@@ -126,17 +153,23 @@ const drawChart = () => {
|
||||
const firstY = height - (historyData.value[0] / 100) * height;
|
||||
ctx.lineTo(startX, firstY);
|
||||
|
||||
// 使用二次贝塞尔曲线平滑曲线
|
||||
// 绘制数据点路径 - 使用曲线连接点,确保连续性
|
||||
for (let i = 1; i < dataLength; i++) {
|
||||
const x = startX + i * stepX;
|
||||
const y = height - (historyData.value[i] / 100) * height;
|
||||
|
||||
// 使用贝塞尔曲线平滑连接
|
||||
if (i < dataLength - 1) {
|
||||
const nextX = startX + (i + 1) * stepX;
|
||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
||||
const controlX = x + stepX / 2;
|
||||
const controlY = y;
|
||||
ctx.quadraticCurveTo(controlX, controlY, (x + nextX) / 2, (y + nextY) / 2);
|
||||
const cpX1 = x - stepX / 4;
|
||||
const cpY1 = y;
|
||||
const cpX2 = x + stepX / 4;
|
||||
const cpY2 = nextY;
|
||||
|
||||
// 使用三次贝塞尔曲线平滑连接点
|
||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
@@ -146,68 +179,55 @@ const drawChart = () => {
|
||||
const lastX = startX + (dataLength - 1) * stepX;
|
||||
ctx.lineTo(lastX, height);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fill();
|
||||
|
||||
// 绘制主曲线 - 从上到下逐渐清晰
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// 分段绘制曲线,每段有不同的透明度
|
||||
const segments = 10;
|
||||
for (let seg = 0; seg < segments; seg++) {
|
||||
const segmentStart = seg / segments;
|
||||
const segmentEnd = (seg + 1) / segments;
|
||||
const opacity = 0.3 + (seg / segments) * 0.7; // 从上0.3到下1.0
|
||||
|
||||
ctx.strokeStyle = `rgba(74, 158, 255, ${opacity})`;
|
||||
ctx.lineWidth = 1.5 + (seg / segments) * 0.8; // 线条也从细到粗
|
||||
|
||||
// 绘制主曲线 - 平滑连续的曲线
|
||||
ctx.beginPath();
|
||||
let segmentStarted = false;
|
||||
ctx.moveTo(startX, firstY);
|
||||
|
||||
for (let i = 0; i < dataLength; i++) {
|
||||
// 重新绘制曲线路径,但这次只绘制线条
|
||||
for (let i = 1; i < dataLength; i++) {
|
||||
const x = startX + i * stepX;
|
||||
const y = height - (historyData.value[i] / 100) * height;
|
||||
const yPercent = 1 - (y / height);
|
||||
|
||||
if (yPercent >= segmentStart && yPercent <= segmentEnd) {
|
||||
if (!segmentStarted) {
|
||||
ctx.moveTo(x, y);
|
||||
segmentStarted = true;
|
||||
} else {
|
||||
// 使用贝塞尔曲线平滑连接
|
||||
if (i < dataLength - 1) {
|
||||
const nextX = startX + (i + 1) * stepX;
|
||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
||||
const controlX = x + stepX / 2;
|
||||
const controlY = y;
|
||||
ctx.quadraticCurveTo(controlX, controlY, (x + nextX) / 2, (y + nextY) / 2);
|
||||
const cpX1 = x - stepX / 4;
|
||||
const cpY1 = y;
|
||||
const cpX2 = x + stepX / 4;
|
||||
const cpY2 = nextY;
|
||||
|
||||
// 使用三次贝塞尔曲线平滑连接点
|
||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (segmentStarted) {
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制当前值的高亮点 - 根据位置调整透明度
|
||||
// 绘制当前值的高亮点
|
||||
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
|
||||
const pointOpacity = 0.4 + (1 - lastY / height) * 0.6;
|
||||
|
||||
// 外圈
|
||||
ctx.fillStyle = `rgba(74, 158, 255, ${pointOpacity * 0.3})`;
|
||||
ctx.fillStyle = pointColor;
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 4, 0, Math.PI * 2);
|
||||
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 内圈
|
||||
ctx.fillStyle = `rgba(74, 158, 255, ${pointOpacity})`;
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 2, 0, Math.PI * 2);
|
||||
ctx.arc(lastX, lastY, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
@@ -228,31 +248,59 @@ const handleResize = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 仅监听系统主题变化
|
||||
const setupSystemThemeListener = () => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleSystemThemeChange = () => {
|
||||
// 仅当设置为auto时才响应系统主题变化
|
||||
if (themeStore.currentTheme === SystemThemeType.SystemThemeAuto) {
|
||||
nextTick(() => drawChart());
|
||||
}
|
||||
};
|
||||
|
||||
// 添加监听器
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (mediaQuery.removeEventListener) {
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMemoryStats();
|
||||
// 每3秒更新一次内存信息
|
||||
// 每1秒更新一次内存信息
|
||||
intervalId = setInterval(fetchMemoryStats, 3000);
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 设置系统主题监听器(仅用于auto模式)
|
||||
const cleanupThemeListener = setupSystemThemeListener();
|
||||
|
||||
// 在卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cleanupThemeListener();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="memory-monitor" @click="triggerGC" :title="`内存: ${formattedMemory} | 点击清理内存`">
|
||||
<div class="memory-monitor" @click="triggerGC" :title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
|
||||
<div class="monitor-info">
|
||||
<div class="memory-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
<span>内存</span>
|
||||
<span>{{ t('monitor.memory') }}</span>
|
||||
</div>
|
||||
<div class="memory-value" v-if="!isLoading">{{ formattedMemory }}</div>
|
||||
<div class="memory-loading" v-else>--</div>
|
||||
@@ -279,11 +327,11 @@ onUnmounted(() => {
|
||||
&:hover {
|
||||
.monitor-info {
|
||||
.memory-label {
|
||||
color: #4a9eff;
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
.memory-value {
|
||||
color: #ffffff;
|
||||
color: var(--toolbar-text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +349,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #a0a0a0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
@@ -318,7 +366,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.memory-value, .memory-loading {
|
||||
color: #e0e0e0;
|
||||
color: var(--toolbar-text-secondary);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
|
@@ -95,7 +95,7 @@ const selectDoc = async (doc: Document) => {
|
||||
closeMenu();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换文档失败:', error);
|
||||
console.error('Failed to switch documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@ const createDoc = async (title: string) => {
|
||||
await selectDoc(newDoc);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文档失败:', error);
|
||||
console.error('Failed to create document:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {onMounted, onUnmounted, ref, watch} from 'vue';
|
||||
import {onMounted, onUnmounted, ref, watch, computed} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useRouter} from 'vue-router';
|
||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||
@@ -16,7 +15,6 @@ import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode
|
||||
const editorStore = useEditorStore();
|
||||
const configStore = useConfigStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const {t} = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -154,6 +152,25 @@ watch(isLoaded, async (loaded) => {
|
||||
await setWindowAlwaysOnTop(true);
|
||||
}
|
||||
});
|
||||
|
||||
const handleUpdateButtonClick = async () => {
|
||||
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
||||
// 开始下载更新
|
||||
await updateStore.applyUpdate();
|
||||
} else if (updateStore.updateSuccess) {
|
||||
// 更新成功后,点击重启
|
||||
await updateStore.restartApplication();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新按钮标题计算属性
|
||||
const updateButtonTitle = computed(() => {
|
||||
if (updateStore.isChecking) return t('settings.checking');
|
||||
if (updateStore.isUpdating) return t('settings.updating');
|
||||
if (updateStore.updateSuccess) return t('settings.updateSuccessRestartRequired');
|
||||
if (updateStore.hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateStore.updateResult?.latestVersion || ''}`;
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -203,14 +220,41 @@ watch(isLoaded, async (loaded) => {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 更新提示图标 -->
|
||||
<!-- 更新按钮 - 根据状态显示不同图标 -->
|
||||
<div
|
||||
v-if="updateStore.hasUpdate"
|
||||
v-if="updateStore.hasUpdate || updateStore.isChecking || updateStore.isUpdating || updateStore.updateSuccess"
|
||||
class="update-button"
|
||||
:title="`发现新版本 ${updateStore.updateResult?.latestVer || ''}`"
|
||||
@click="updateStore.openReleaseURL"
|
||||
:class="{
|
||||
'checking': updateStore.isChecking,
|
||||
'updating': updateStore.isUpdating,
|
||||
'success': updateStore.updateSuccess,
|
||||
'available': updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess
|
||||
}"
|
||||
:title="updateButtonTitle"
|
||||
@click="handleUpdateButtonClick"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
<!-- 检查更新中 -->
|
||||
<svg v-if="updateStore.isChecking" 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" class="rotating">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||
</svg>
|
||||
|
||||
<!-- 下载更新中 -->
|
||||
<svg v-else-if="updateStore.isUpdating" 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" class="rotating">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
||||
<path d="M12 2a10 10 0 1 0 10 10"></path>
|
||||
</svg>
|
||||
|
||||
<!-- 更新成功,等待重启 -->
|
||||
<svg v-else-if="updateStore.updateSuccess" 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" class="pulsing">
|
||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
|
||||
<line x1="12" y1="2" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
|
||||
<!-- 有更新可用 -->
|
||||
<svg v-else 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">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="7.5,10.5 12,15 16.5,10.5"/>
|
||||
@@ -282,8 +326,7 @@ watch(isLoaded, async (loaded) => {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
|
||||
/* 更新提示按钮样式 */
|
||||
/* 更新按钮样式 */
|
||||
.update-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -293,17 +336,58 @@ watch(isLoaded, async (loaded) => {
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
/* 有更新可用状态 */
|
||||
&.available {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
svg {
|
||||
stroke: #4caf50;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* 检查更新中状态 */
|
||||
&.checking {
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
|
||||
svg {
|
||||
stroke: #4caf50;
|
||||
stroke: #ffc107;
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新下载中状态 */
|
||||
&.updating {
|
||||
background-color: rgba(33, 150, 243, 0.1);
|
||||
|
||||
svg {
|
||||
stroke: #2196f3;
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新成功状态 */
|
||||
&.success {
|
||||
background-color: rgba(156, 39, 176, 0.1);
|
||||
|
||||
svg {
|
||||
stroke: #9c27b0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.rotating {
|
||||
animation: rotate 1.5s linear infinite;
|
||||
}
|
||||
|
||||
/* 脉冲动画 */
|
||||
.pulsing {
|
||||
animation: pulse-strong 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -314,6 +398,26 @@ watch(isLoaded, async (loaded) => {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-strong {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 窗口置顶图标按钮样式 */
|
||||
|
@@ -156,15 +156,15 @@ export default {
|
||||
saveOptions: 'Save Options',
|
||||
autoSaveDelay: 'Auto Save Delay (ms)',
|
||||
updateSettings: 'Update Settings',
|
||||
autoCheckUpdates: 'Auto Check Updates',
|
||||
autoCheckUpdatesDescription: 'Automatically check for updates on startup',
|
||||
manualCheck: 'Manual Check',
|
||||
autoCheckUpdates: 'Automatically Check Updates',
|
||||
autoCheckUpdatesDescription: 'Check for updates when application starts',
|
||||
manualCheck: 'Manual Update',
|
||||
currentVersion: 'Current Version',
|
||||
checkForUpdates: 'Check for Updates',
|
||||
checking: 'Checking...',
|
||||
checkFailed: 'Check Failed',
|
||||
newVersionAvailable: 'New Version Available',
|
||||
upToDate: 'You are using the latest version',
|
||||
upToDate: 'Up to Date',
|
||||
viewUpdate: 'View Update',
|
||||
releaseNotes: 'Release Notes',
|
||||
networkError: 'Network connection error, please check your network settings',
|
||||
@@ -177,7 +177,12 @@ export default {
|
||||
configuration: 'Configuration',
|
||||
resetToDefault: 'Reset to Default Configuration',
|
||||
// Keep necessary extension interface translations, configuration items display in English directly
|
||||
}
|
||||
},
|
||||
updateNow: 'Update Now',
|
||||
updating: 'Updating...',
|
||||
updateSuccess: 'Update Success',
|
||||
updateSuccessRestartRequired: 'Update has been successfully applied. Please restart the application.',
|
||||
restartNow: 'Restart Now',
|
||||
},
|
||||
extensions: {
|
||||
rainbowBrackets: {
|
||||
@@ -216,5 +221,9 @@ export default {
|
||||
name: 'Checkbox',
|
||||
description: 'Render [x] and [ ] as interactive checkboxes'
|
||||
}
|
||||
},
|
||||
monitor: {
|
||||
memory: 'Memory',
|
||||
clickToClean: 'Click to clean memory'
|
||||
}
|
||||
};
|
@@ -158,17 +158,23 @@ export default {
|
||||
autoSaveDelay: '自动保存延迟(毫秒)',
|
||||
updateSettings: '更新设置',
|
||||
autoCheckUpdates: '自动检查更新',
|
||||
autoCheckUpdatesDescription: '启动应用时自动检查更新',
|
||||
manualCheck: '手动检查',
|
||||
autoCheckUpdatesDescription: '应用启动时自动检查更新',
|
||||
manualCheck: '手动更新',
|
||||
currentVersion: '当前版本',
|
||||
checkForUpdates: '检查更新',
|
||||
checking: '检查中...',
|
||||
checking: '正在检查...',
|
||||
checkFailed: '检查失败',
|
||||
newVersionAvailable: '发现新版本',
|
||||
upToDate: '您正在使用最新版本',
|
||||
upToDate: '已是最新版本',
|
||||
viewUpdate: '查看更新',
|
||||
releaseNotes: '更新内容',
|
||||
releaseNotes: '更新日志',
|
||||
networkError: '网络连接错误,请检查网络设置',
|
||||
updateNow: '立即更新',
|
||||
updating: '正在更新...',
|
||||
updateSuccess: '更新成功',
|
||||
updateSuccessRestartRequired: '更新已成功应用,请重启应用以生效',
|
||||
updateSuccessNoRestart: '更新已完成,无需重启',
|
||||
restartNow: '立即重启',
|
||||
extensionsPage: {
|
||||
loading: '加载中',
|
||||
categoryEditing: '编辑增强',
|
||||
@@ -216,5 +222,9 @@ export default {
|
||||
name: '选择框',
|
||||
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
|
||||
}
|
||||
},
|
||||
monitor: {
|
||||
memory: '内存',
|
||||
clickToClean: '点击清理内存'
|
||||
}
|
||||
};
|
@@ -10,6 +10,7 @@ import {
|
||||
SystemThemeType,
|
||||
TabType,
|
||||
UpdatesConfig,
|
||||
UpdateSourceType,
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {ConfigUtils} from '@/utils/configUtils';
|
||||
@@ -77,7 +78,13 @@ const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
|
||||
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
version: 'updates.version',
|
||||
autoUpdate: 'updates.autoUpdate'
|
||||
autoUpdate: 'updates.autoUpdate',
|
||||
primarySource: 'updates.primarySource',
|
||||
backupSource: 'updates.backupSource',
|
||||
backupBeforeUpdate: 'updates.backupBeforeUpdate',
|
||||
updateTimeout: 'updates.updateTimeout',
|
||||
github: 'updates.github',
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
// 配置限制
|
||||
@@ -155,6 +162,19 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
autoUpdate: true,
|
||||
primarySource: UpdateSourceType.UpdateSourceGithub,
|
||||
backupSource: UpdateSourceType.UpdateSourceGitea,
|
||||
backupBeforeUpdate: true,
|
||||
updateTimeout: 30,
|
||||
github: {
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
},
|
||||
gitea: {
|
||||
baseURL: "https://git.landaiqing.cn",
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
|
@@ -1,43 +1,90 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {computed, ref} from 'vue'
|
||||
import {CheckForUpdates} from '@/../bindings/voidraft/internal/services/updateservice'
|
||||
import {UpdateCheckResult} from '@/../bindings/voidraft/internal/services/models'
|
||||
import {CheckForUpdates, ApplyUpdate, RestartApplication} from '@/../bindings/voidraft/internal/services/selfupdateservice'
|
||||
import {SelfUpdateResult} from '@/../bindings/voidraft/internal/services/models'
|
||||
import {useConfigStore} from './configStore'
|
||||
import * as runtime from "@wailsio/runtime"
|
||||
|
||||
export const useUpdateStore = defineStore('update', () => {
|
||||
// 状态
|
||||
const isChecking = ref(false)
|
||||
const updateResult = ref<UpdateCheckResult | null>(null)
|
||||
const isUpdating = ref(false)
|
||||
const updateResult = ref<SelfUpdateResult | null>(null)
|
||||
const hasCheckedOnStartup = ref(false)
|
||||
const updateSuccess = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 计算属性
|
||||
const hasUpdate = computed(() => updateResult.value?.hasUpdate || false)
|
||||
const errorMessage = computed(() => updateResult.value?.error || '')
|
||||
|
||||
// 检查更新
|
||||
const checkForUpdates = async (): Promise<boolean> => {
|
||||
if (isChecking.value) return false
|
||||
|
||||
// 重置错误信息
|
||||
errorMessage.value = ''
|
||||
isChecking.value = true
|
||||
try {
|
||||
const result = await CheckForUpdates()
|
||||
if (result) {
|
||||
updateResult.value = result
|
||||
return !result.error
|
||||
if (result.error) {
|
||||
errorMessage.value = result.error
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
updateResult.value = new UpdateCheckResult({
|
||||
hasUpdate: false,
|
||||
currentVer: '1.0.0',
|
||||
latestVer: '',
|
||||
releaseNotes: '',
|
||||
releaseURL: '',
|
||||
error: 'Network error'
|
||||
})
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Network error'
|
||||
return false
|
||||
} finally {
|
||||
isChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 应用更新
|
||||
const applyUpdate = async (): Promise<boolean> => {
|
||||
if (isUpdating.value) return false
|
||||
|
||||
// 重置错误信息
|
||||
errorMessage.value = ''
|
||||
isUpdating.value = true
|
||||
try {
|
||||
const result = await ApplyUpdate()
|
||||
if (result) {
|
||||
updateResult.value = result
|
||||
|
||||
if (result.error) {
|
||||
errorMessage.value = result.error
|
||||
return false
|
||||
}
|
||||
|
||||
if (result.updateApplied) {
|
||||
updateSuccess.value = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Update failed'
|
||||
return false
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重启应用
|
||||
const restartApplication = async (): Promise<boolean> => {
|
||||
try {
|
||||
await RestartApplication()
|
||||
return true
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Restart failed'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 启动时检查更新
|
||||
const checkOnStartup = async () => {
|
||||
if (hasCheckedOnStartup.value) return
|
||||
@@ -50,9 +97,9 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
}
|
||||
|
||||
// 打开发布页面
|
||||
const openReleaseURL = () => {
|
||||
if (updateResult.value?.releaseURL) {
|
||||
window.open(updateResult.value.releaseURL, '_blank')
|
||||
const openReleaseURL = async () => {
|
||||
if (updateResult.value?.assetURL) {
|
||||
await runtime.Browser.OpenURL(updateResult.value.assetURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,20 +107,27 @@ export const useUpdateStore = defineStore('update', () => {
|
||||
const reset = () => {
|
||||
updateResult.value = null
|
||||
isChecking.value = false
|
||||
isUpdating.value = false
|
||||
updateSuccess.value = false
|
||||
errorMessage.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isChecking,
|
||||
isUpdating,
|
||||
updateResult,
|
||||
hasCheckedOnStartup,
|
||||
updateSuccess,
|
||||
errorMessage,
|
||||
|
||||
// 计算属性
|
||||
hasUpdate,
|
||||
errorMessage,
|
||||
|
||||
// 方法
|
||||
checkForUpdates,
|
||||
applyUpdate,
|
||||
restartApplication,
|
||||
checkOnStartup,
|
||||
openReleaseURL,
|
||||
reset
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view';
|
||||
import { Extension, Range } from '@codemirror/state';
|
||||
|
||||
import * as runtime from "@wailsio/runtime"
|
||||
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
|
||||
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi;
|
||||
|
||||
@@ -201,7 +201,8 @@ export const hyperLinkClickHandler = EditorView.domEventHandlers({
|
||||
if (target.classList.contains('cm-hyper-link-text')) {
|
||||
const url = target.getAttribute('data-url');
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
// window.open(url, '_blank', 'noopener,noreferrer');
|
||||
runtime.Browser.OpenURL(url).then()
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
@@ -1,16 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, onMounted} from 'vue';
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import { Remarkable } from 'remarkable';
|
||||
|
||||
const {t} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const updateStore = useUpdateStore();
|
||||
|
||||
// 初始化Remarkable实例并配置
|
||||
const md = new Remarkable({
|
||||
html: true, // 允许HTML
|
||||
xhtmlOut: false, // 不使用'/'闭合单标签
|
||||
breaks: true, // 将'\n'转换为<br>
|
||||
typographer: true // 启用排版增强
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const autoCheckUpdates = computed({
|
||||
get: () => configStore.config.updates.autoUpdate,
|
||||
@@ -19,30 +28,30 @@ const autoCheckUpdates = computed({
|
||||
}
|
||||
});
|
||||
|
||||
// 格式化发布说明
|
||||
const formatReleaseNotes = (notes: string) => {
|
||||
if (!notes) return [];
|
||||
|
||||
// 简单的Markdown列表解析
|
||||
return notes
|
||||
.split('\n')
|
||||
.filter(line => line.trim().startsWith('-') || line.trim().startsWith('*'))
|
||||
.map(line => line.replace(/^[\s\-\*]+/, '').trim())
|
||||
.filter(line => line.length > 0);
|
||||
// 使用Remarkable解析Markdown
|
||||
const parseMarkdown = (markdown: string) => {
|
||||
if (!markdown) return '';
|
||||
return md.render(markdown);
|
||||
};
|
||||
|
||||
// 处理查看更新
|
||||
const viewUpdate = () => {
|
||||
updateStore.openReleaseURL();
|
||||
};
|
||||
|
||||
// 获取错误信息的国际化文本
|
||||
const getErrorMessage = (error: string) => {
|
||||
if (error.includes('Network') || error.includes('network')) {
|
||||
return t('settings.networkError');
|
||||
// 处理更新按钮点击
|
||||
const handleUpdateButtonClick = async () => {
|
||||
if (updateStore.updateSuccess) {
|
||||
// 如果更新成功,点击按钮重启应用
|
||||
await updateStore.restartApplication();
|
||||
} else if (updateStore.hasUpdate) {
|
||||
// 如果有更新,点击按钮应用更新
|
||||
await updateStore.applyUpdate();
|
||||
} else {
|
||||
// 否则检查更新
|
||||
await updateStore.checkForUpdates();
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
// 当前版本号
|
||||
const currentVersion = computed(() => {
|
||||
return updateStore.updateResult?.currentVersion || configStore.config.updates.version;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,15 +69,26 @@ const getErrorMessage = (error: string) => {
|
||||
<!-- 手动检查更新 -->
|
||||
<SettingSection :title="t('settings.manualCheck')">
|
||||
<SettingItem
|
||||
:title="`${t('settings.currentVersion')}: ${updateStore.updateResult?.currentVer || configStore.config.updates.version}`"
|
||||
:title="`${t('settings.currentVersion')}: ${currentVersion}`"
|
||||
>
|
||||
<button
|
||||
class="check-button"
|
||||
@click="updateStore.checkForUpdates"
|
||||
:disabled="updateStore.isChecking"
|
||||
:class="{
|
||||
'update-available-button': updateStore.hasUpdate && !updateStore.updateSuccess,
|
||||
'update-success-button': updateStore.updateSuccess
|
||||
}"
|
||||
@click="handleUpdateButtonClick"
|
||||
:disabled="updateStore.isChecking || updateStore.isUpdating"
|
||||
>
|
||||
<span v-if="updateStore.isChecking" class="loading-spinner"></span>
|
||||
{{ updateStore.isChecking ? t('settings.checking') : t('settings.checkForUpdates') }}
|
||||
<span v-if="updateStore.isChecking || updateStore.isUpdating" class="loading-spinner"></span>
|
||||
{{ updateStore.isChecking
|
||||
? t('settings.checking')
|
||||
: (updateStore.isUpdating
|
||||
? t('settings.updating')
|
||||
: (updateStore.updateSuccess
|
||||
? t('settings.restartNow')
|
||||
: (updateStore.hasUpdate ? t('settings.updateNow') : t('settings.checkForUpdates'))))
|
||||
}}
|
||||
</button>
|
||||
</SettingItem>
|
||||
|
||||
@@ -78,42 +98,40 @@ const getErrorMessage = (error: string) => {
|
||||
<div v-if="updateStore.errorMessage" class="result-item error-result">
|
||||
<div class="result-text">
|
||||
<span class="result-icon">⚠️</span>
|
||||
<span class="result-message">{{ getErrorMessage(updateStore.errorMessage) }}</span>
|
||||
<div class="result-message">{{ updateStore.errorMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新成功 -->
|
||||
<div v-else-if="updateStore.updateSuccess" class="result-item update-success">
|
||||
<div class="result-text">
|
||||
<span class="result-icon">✅</span>
|
||||
<span class="result-message">
|
||||
{{ t('settings.updateSuccessRestartRequired') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 有新版本 -->
|
||||
<div v-else-if="updateStore.hasUpdate" class="result-item update-result">
|
||||
<div class="result-header">
|
||||
<div class="result-text">
|
||||
<span class="result-icon">🎉</span>
|
||||
<span class="result-message">
|
||||
{{ t('settings.newVersionAvailable') }}: {{ updateStore.updateResult?.latestVer }}
|
||||
{{ t('settings.newVersionAvailable') }}: {{ updateStore.updateResult?.latestVersion }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="view-button" @click="viewUpdate">
|
||||
{{ t('settings.viewUpdate') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="updateStore.updateResult?.releaseNotes" class="release-notes">
|
||||
<div class="notes-title">{{ t('settings.releaseNotes') }}:</div>
|
||||
<ul class="notes-list" v-if="formatReleaseNotes(updateStore.updateResult.releaseNotes).length > 0">
|
||||
<li v-for="(note, index) in formatReleaseNotes(updateStore.updateResult.releaseNotes)" :key="index">
|
||||
{{ note }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="notes-text">
|
||||
{{ updateStore.updateResult.releaseNotes }}
|
||||
</div>
|
||||
<div class="markdown-content" v-html="parseMarkdown(updateStore.updateResult.releaseNotes)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已是最新版本 -->
|
||||
<div v-else-if="updateStore.updateResult && !updateStore.hasUpdate && !updateStore.errorMessage"
|
||||
class="result-item success-result">
|
||||
class="result-item latest-version">
|
||||
<div class="result-text">
|
||||
<span class="result-icon">✅</span>
|
||||
<span class="result-icon">✓</span>
|
||||
<span class="result-message">{{ t('settings.upToDate') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,6 +143,7 @@ const getErrorMessage = (error: string) => {
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
width: 100%; // 确保在小屏幕上也能占满可用空间
|
||||
}
|
||||
|
||||
.check-button {
|
||||
@@ -139,6 +158,8 @@ const getErrorMessage = (error: string) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 120px;
|
||||
justify-content: center;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--settings-hover);
|
||||
@@ -164,6 +185,28 @@ const getErrorMessage = (error: string) => {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
&.update-available-button {
|
||||
background-color: #2196f3;
|
||||
border-color: #2196f3;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #1976d2;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
}
|
||||
|
||||
&.update-success-button {
|
||||
background-color: #4caf50;
|
||||
border-color: #4caf50;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #43a047;
|
||||
border-color: #43a047;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
@@ -172,122 +215,157 @@ const getErrorMessage = (error: string) => {
|
||||
}
|
||||
|
||||
.check-results {
|
||||
padding: 0 16px;
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
|
||||
// 为错误消息添加特殊样式
|
||||
.error-result {
|
||||
padding: 12px;
|
||||
background-color: rgba(255, 82, 82, 0.05);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--error-text, #ff5252);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.result-message {
|
||||
color: var(--error-text, #ff5252);
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
padding-right: 8px; // 添加右侧内边距,防止文本贴近容器边缘
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 12px 0;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.result-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5; // 增加行高,提高可读性
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.view-button {
|
||||
padding: 4px 12px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--settings-text);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--settings-hover);
|
||||
border-color: var(--settings-border);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.release-notes {
|
||||
margin-top: 8px;
|
||||
padding-left: 24px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||
|
||||
.notes-title {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: var(--settings-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notes-list {
|
||||
margin: 0;
|
||||
.markdown-content {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text);
|
||||
line-height: 1.4;
|
||||
|
||||
/* Markdown内容样式 */
|
||||
:deep(p) {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 6px 0;
|
||||
padding-left: 16px;
|
||||
|
||||
li {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text-secondary);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 3px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notes-text {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text-secondary);
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
:deep(li) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||
margin: 10px 0 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-result {
|
||||
.result-message {
|
||||
color: #f44336;
|
||||
}
|
||||
background-color: rgba(244, 67, 54, 0.03);
|
||||
|
||||
.result-icon {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
color: var(--error-text, #ff5252);
|
||||
}
|
||||
}
|
||||
|
||||
.update-result {
|
||||
background-color: rgba(33, 150, 243, 0.03);
|
||||
|
||||
.result-icon {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
color: #2196f3;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.update-success {
|
||||
background-color: rgba(76, 175, 80, 0.03);
|
||||
|
||||
.result-icon {
|
||||
color: #2196f3;
|
||||
}
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.success-result {
|
||||
.result-message {
|
||||
color: #4caf50;
|
||||
color: var(--settings-text);
|
||||
}
|
||||
}
|
||||
|
||||
.latest-version {
|
||||
background-color: transparent;
|
||||
border-left: 3px solid #9e9e9e;
|
||||
padding-left: 10px;
|
||||
|
||||
.result-icon {
|
||||
color: #4caf50;
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
color: var(--settings-text-secondary, #757575);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式布局调整
|
||||
@media (max-width: 600px) {
|
||||
.result-item {
|
||||
padding: 10px;
|
||||
|
||||
.result-text {
|
||||
font-size: 12px; // 小屏幕上稍微减小字体
|
||||
}
|
||||
}
|
||||
|
||||
.check-button {
|
||||
min-width: 100px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
42
go.mod
42
go.mod
@@ -1,34 +1,36 @@
|
||||
module voidraft
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.2
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/google/go-github/v63 v63.0.0
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0
|
||||
github.com/knadh/koanf/parsers/json v1.0.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/structs v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.2.1
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
||||
golang.org/x/sys v0.33.0
|
||||
modernc.org/sqlite v1.21.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||
@@ -36,11 +38,14 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
@@ -51,6 +56,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -58,25 +64,21 @@ require (
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.21 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.2 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
123
go.sum
123
go.sum
@@ -1,7 +1,11 @@
|
||||
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@@ -17,11 +21,15 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU=
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
@@ -30,12 +38,16 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
@@ -52,23 +64,31 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
|
||||
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
@@ -101,12 +121,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
@@ -117,7 +137,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -137,27 +156,42 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 h1:b8CfRrhPno8Fra0xFp4Ifyj+ogmXBc35rsQWvcrHtsI=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.9/go.mod h1:dSv6s722nSWaUyUiapAM1DHc5HKggNGY1a79shO85/g=
|
||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -172,13 +206,18 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -191,31 +230,29 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
|
||||
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
|
||||
modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.1.2 h1:9mfG19tFBypPnlSKRAjI5nXGMLmVy+jLyKNVKsMzt/8=
|
||||
modernc.org/goabi0 v0.1.2/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.2 h1:JCBxlJzZOIwZY54fzjHN3Wsn8Ty5PUTPr/xioRkmecI=
|
||||
modernc.org/libc v1.66.2/go.mod h1:ceIGzvXxP+JV3pgVjP9avPZo6Chlsfof2egXBH3YT5Q=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
@@ -38,6 +38,29 @@ const (
|
||||
SystemThemeAuto SystemThemeType = "auto"
|
||||
)
|
||||
|
||||
// UpdateSourceType 更新源类型
|
||||
type UpdateSourceType string
|
||||
|
||||
const (
|
||||
// UpdateSourceGithub GitHub更新源
|
||||
UpdateSourceGithub UpdateSourceType = "github"
|
||||
// UpdateSourceGitea Gitea更新源
|
||||
UpdateSourceGitea UpdateSourceType = "gitea"
|
||||
)
|
||||
|
||||
// GithubConfig GitHub配置
|
||||
type GithubConfig struct {
|
||||
Owner string `json:"owner"` // 仓库所有者
|
||||
Repo string `json:"repo"` // 仓库名称
|
||||
}
|
||||
|
||||
// GiteaConfig Gitea配置
|
||||
type GiteaConfig struct {
|
||||
BaseURL string `json:"baseURL"` // Gitea服务器URL
|
||||
Owner string `json:"owner"` // 仓库所有者
|
||||
Repo string `json:"repo"` // 仓库名称
|
||||
}
|
||||
|
||||
// GeneralConfig 通用设置配置
|
||||
type GeneralConfig struct {
|
||||
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
|
||||
@@ -86,6 +109,12 @@ type AppearanceConfig struct {
|
||||
type UpdatesConfig struct {
|
||||
Version string `json:"version"` // 当前版本号
|
||||
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
|
||||
PrimarySource UpdateSourceType `json:"primarySource"` // 主要更新源
|
||||
BackupSource UpdateSourceType `json:"backupSource"` // 备用更新源
|
||||
BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份
|
||||
UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
|
||||
Github GithubConfig `json:"github"` // GitHub配置
|
||||
Gitea GiteaConfig `json:"gitea"` // Gitea配置
|
||||
}
|
||||
|
||||
// AppConfig 应用配置 - 按照前端设置页面分类组织
|
||||
@@ -142,8 +171,21 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
SystemTheme: SystemThemeAuto, // 默认使用深色系统主题
|
||||
},
|
||||
Updates: UpdatesConfig{
|
||||
Version: "1.0.0",
|
||||
Version: "0.0.0",
|
||||
AutoUpdate: true,
|
||||
PrimarySource: UpdateSourceGithub,
|
||||
BackupSource: UpdateSourceGitea,
|
||||
BackupBeforeUpdate: true,
|
||||
UpdateTimeout: 30,
|
||||
Github: GithubConfig{
|
||||
Owner: "landaiqing",
|
||||
Repo: "voidraft",
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
BaseURL: "https://git.landaiqing.cn",
|
||||
Owner: "landaiqing",
|
||||
Repo: "voidraft",
|
||||
},
|
||||
},
|
||||
Metadata: ConfigMetadata{
|
||||
LastUpdated: time.Now().Format(time.RFC3339),
|
||||
|
530
internal/services/self_update_service.go
Normal file
530
internal/services/self_update_service.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/creativeprojects/go-selfupdate"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
)
|
||||
|
||||
// SelfUpdateResult 自我更新结果
|
||||
type SelfUpdateResult struct {
|
||||
HasUpdate bool `json:"hasUpdate"` // 是否有更新
|
||||
CurrentVersion string `json:"currentVersion"` // 当前版本
|
||||
LatestVersion string `json:"latestVersion"` // 最新版本
|
||||
UpdateApplied bool `json:"updateApplied"` // 是否已应用更新
|
||||
AssetURL string `json:"assetURL"` // 下载链接
|
||||
ReleaseNotes string `json:"releaseNotes"` // 发布说明
|
||||
Error string `json:"error"` // 错误信息
|
||||
Source string `json:"source"` // 更新源(github/gitea)
|
||||
}
|
||||
|
||||
// SelfUpdateService 自我更新服务
|
||||
type SelfUpdateService struct {
|
||||
logger *log.LoggerService
|
||||
configService *ConfigService
|
||||
config *models.AppConfig
|
||||
|
||||
// 状态管理
|
||||
isUpdating bool
|
||||
}
|
||||
|
||||
// NewSelfUpdateService 创建自我更新服务实例
|
||||
func NewSelfUpdateService(configService *ConfigService, logger *log.LoggerService) (*SelfUpdateService, error) {
|
||||
// 获取配置
|
||||
appConfig, err := configService.GetConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
service := &SelfUpdateService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
config: appConfig,
|
||||
isUpdating: false,
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// CheckForUpdates 检查更新
|
||||
func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateResult, error) {
|
||||
result := &SelfUpdateResult{
|
||||
CurrentVersion: s.config.Updates.Version,
|
||||
HasUpdate: false,
|
||||
UpdateApplied: false,
|
||||
}
|
||||
|
||||
// 首先尝试主要更新源
|
||||
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
|
||||
if err == nil && primaryResult != nil {
|
||||
return primaryResult, nil
|
||||
}
|
||||
|
||||
// 如果主要更新源失败,尝试备用更新源
|
||||
backupResult, backupErr := s.checkSourceForUpdates(ctx, s.config.Updates.BackupSource)
|
||||
if backupErr != nil {
|
||||
// 如果备用源也失败,返回主要源的错误信息
|
||||
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
|
||||
return result, errors.New(result.Error)
|
||||
}
|
||||
|
||||
return backupResult, nil
|
||||
}
|
||||
|
||||
// checkSourceForUpdates 根据更新源类型检查更新
|
||||
func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType) (*SelfUpdateResult, error) {
|
||||
// 创建带超时的上下文
|
||||
timeout := s.config.Updates.UpdateTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30 // 默认30秒
|
||||
}
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := &SelfUpdateResult{
|
||||
CurrentVersion: s.config.Updates.Version,
|
||||
HasUpdate: false,
|
||||
UpdateApplied: false,
|
||||
Source: string(sourceType),
|
||||
}
|
||||
|
||||
var release *selfupdate.Release
|
||||
var found bool
|
||||
var err error
|
||||
|
||||
switch sourceType {
|
||||
case models.UpdateSourceGithub:
|
||||
release, found, err = s.checkGithubUpdates(timeoutCtx)
|
||||
case models.UpdateSourceGitea:
|
||||
release, found, err = s.checkGiteaUpdates(timeoutCtx)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported update source type: %s", sourceType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to check for updates: %v", err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
if !found {
|
||||
result.Error = fmt.Sprintf("No release found for %s/%s on %s",
|
||||
runtime.GOOS, runtime.GOARCH, s.getRepoName(sourceType))
|
||||
return result, errors.New(result.Error)
|
||||
}
|
||||
|
||||
result.LatestVersion = release.Version()
|
||||
result.AssetURL = release.AssetURL
|
||||
result.ReleaseNotes = release.ReleaseNotes
|
||||
|
||||
// 比较版本
|
||||
if release.GreaterThan(s.config.Updates.Version) {
|
||||
result.HasUpdate = true
|
||||
} else {
|
||||
s.logger.Info("Current version is up to date")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createGithubUpdater 创建GitHub更新器
|
||||
func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) {
|
||||
// 使用默认的GitHub源
|
||||
updaterConfig := selfupdate.Config{}
|
||||
|
||||
return selfupdate.NewUpdater(updaterConfig)
|
||||
}
|
||||
|
||||
// createGiteaUpdater 创建Gitea更新器
|
||||
func (s *SelfUpdateService) createGiteaUpdater() (*selfupdate.Updater, error) {
|
||||
giteaConfig := s.config.Updates.Gitea
|
||||
|
||||
// 创建Gitea源
|
||||
source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{
|
||||
BaseURL: giteaConfig.BaseURL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Gitea source: %w", err)
|
||||
}
|
||||
|
||||
// 创建使用Gitea源的更新器
|
||||
updaterConfig := selfupdate.Config{
|
||||
Source: source,
|
||||
}
|
||||
|
||||
return selfupdate.NewUpdater(updaterConfig)
|
||||
}
|
||||
|
||||
// checkGithubUpdates 检查GitHub更新
|
||||
func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
|
||||
// 创建GitHub更新器
|
||||
updater, err := s.createGithubUpdater()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
|
||||
}
|
||||
|
||||
githubConfig := s.config.Updates.Github
|
||||
repository := selfupdate.NewRepositorySlug(githubConfig.Owner, githubConfig.Repo)
|
||||
|
||||
// 检测最新版本
|
||||
return updater.DetectLatest(ctx, repository)
|
||||
}
|
||||
|
||||
// checkGiteaUpdates 检查Gitea更新
|
||||
func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
|
||||
// 创建Gitea更新器
|
||||
updater, err := s.createGiteaUpdater()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
|
||||
}
|
||||
|
||||
giteaConfig := s.config.Updates.Gitea
|
||||
repository := selfupdate.NewRepositorySlug(giteaConfig.Owner, giteaConfig.Repo)
|
||||
|
||||
// 检测最新版本
|
||||
return updater.DetectLatest(ctx, repository)
|
||||
}
|
||||
|
||||
// getRepoName 获取当前更新源的仓库名称
|
||||
func (s *SelfUpdateService) getRepoName(sourceType models.UpdateSourceType) string {
|
||||
switch sourceType {
|
||||
case models.UpdateSourceGithub:
|
||||
return s.config.Updates.Github.Repo
|
||||
case models.UpdateSourceGitea:
|
||||
return s.config.Updates.Gitea.Repo
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyUpdate 应用更新
|
||||
func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) {
|
||||
if s.isUpdating {
|
||||
return nil, errors.New("update is already in progress")
|
||||
}
|
||||
|
||||
s.isUpdating = true
|
||||
defer func() {
|
||||
s.isUpdating = false
|
||||
}()
|
||||
|
||||
// 获取可执行文件路径
|
||||
exe, err := selfupdate.ExecutablePath()
|
||||
if err != nil {
|
||||
return &SelfUpdateResult{
|
||||
CurrentVersion: s.config.Updates.Version,
|
||||
Error: fmt.Sprintf("Could not locate executable path: %v", err),
|
||||
}, err
|
||||
}
|
||||
|
||||
// 创建带超时的上下文,仅用于检测最新版本
|
||||
timeout := s.config.Updates.UpdateTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30 // 默认30秒
|
||||
}
|
||||
checkTimeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := &SelfUpdateResult{
|
||||
CurrentVersion: s.config.Updates.Version,
|
||||
}
|
||||
|
||||
// 首先尝试从主要更新源获取更新信息
|
||||
primarySourceType := s.config.Updates.PrimarySource
|
||||
backupSourceType := s.config.Updates.BackupSource
|
||||
|
||||
result.Source = string(primarySourceType)
|
||||
|
||||
// 从主更新源获取更新信息
|
||||
primaryUpdater, primaryRelease, primaryFound, err := s.getUpdateFromSource(checkTimeoutCtx, primarySourceType)
|
||||
|
||||
if err != nil || !primaryFound {
|
||||
// 主更新源失败,直接尝试备用源
|
||||
return s.updateFromSource(ctx, backupSourceType, exe)
|
||||
}
|
||||
|
||||
// 检查是否有可用更新
|
||||
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
|
||||
s.logger.Info("Current version is up to date, no need to apply update")
|
||||
result.LatestVersion = primaryRelease.Version()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 更新结果信息
|
||||
result.LatestVersion = primaryRelease.Version()
|
||||
result.AssetURL = primaryRelease.AssetURL
|
||||
result.ReleaseNotes = primaryRelease.ReleaseNotes
|
||||
result.HasUpdate = true
|
||||
|
||||
// 备份当前可执行文件(如果启用)
|
||||
var backupPath string
|
||||
if s.config.Updates.BackupBeforeUpdate {
|
||||
var err error
|
||||
backupPath, err = s.createBackup(exe)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to create backup: %v", err)
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
// 从主要源尝试下载并应用更新,不设置超时
|
||||
err = primaryUpdater.UpdateTo(ctx, primaryRelease, exe)
|
||||
|
||||
// 如果主要源下载失败,尝试备用源
|
||||
if err != nil {
|
||||
// 尝试从备用源更新
|
||||
backupResult, backupErr := s.updateFromSource(ctx, backupSourceType, exe)
|
||||
|
||||
// 如果备用源也失败,清理并返回错误
|
||||
if backupErr != nil {
|
||||
if backupPath != "" {
|
||||
s.cleanupBackup(backupPath)
|
||||
}
|
||||
|
||||
result.Error = fmt.Sprintf("Update failed from both sources: primary error: %v; backup error: %v", err, backupErr)
|
||||
return result, errors.New(result.Error)
|
||||
}
|
||||
|
||||
// 备用源成功
|
||||
return backupResult, nil
|
||||
}
|
||||
|
||||
// 主要源更新成功
|
||||
result.UpdateApplied = true
|
||||
|
||||
// 更新成功后清理备份文件
|
||||
if backupPath != "" {
|
||||
if err := s.cleanupBackup(backupPath); err != nil {
|
||||
s.logger.Error("Failed to cleanup backup", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置中的版本号
|
||||
if err := s.updateConfigVersion(result.LatestVersion); err != nil {
|
||||
s.logger.Error("Failed to update config version", "error", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// updateFromSource 从指定源尝试下载并应用更新
|
||||
func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType models.UpdateSourceType, exe string) (*SelfUpdateResult, error) {
|
||||
// 创建带超时的上下文,仅用于检测最新版本
|
||||
checkTimeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.Updates.UpdateTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := &SelfUpdateResult{
|
||||
CurrentVersion: s.config.Updates.Version,
|
||||
Source: string(sourceType),
|
||||
}
|
||||
|
||||
s.logger.Info("Attempting to update from source", "source", sourceType)
|
||||
|
||||
// 获取更新信息
|
||||
updater, release, found, err := s.getUpdateFromSource(checkTimeoutCtx, sourceType)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to detect latest release from %s: %v", sourceType, err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
if !found {
|
||||
result.Error = fmt.Sprintf("Latest release not found from %s", sourceType)
|
||||
return result, errors.New(result.Error)
|
||||
}
|
||||
|
||||
// 更新结果信息
|
||||
result.LatestVersion = release.Version()
|
||||
result.AssetURL = release.AssetURL
|
||||
result.ReleaseNotes = release.ReleaseNotes
|
||||
|
||||
// 检查是否有更新
|
||||
if !release.GreaterThan(s.config.Updates.Version) {
|
||||
s.logger.Info("Current version is up to date, no need to apply update")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 标记有更新可用
|
||||
result.HasUpdate = true
|
||||
|
||||
// 备份当前可执行文件(如果启用且尚未备份)
|
||||
var backupPath string
|
||||
if s.config.Updates.BackupBeforeUpdate {
|
||||
s.logger.Info("Creating backup before update...")
|
||||
var err error
|
||||
backupPath, err = s.createBackup(exe)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to create backup: %v", err)
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试下载并应用更新,不设置超时
|
||||
s.logger.Info("Downloading update...", "source", sourceType)
|
||||
err = updater.UpdateTo(ctx, release, exe)
|
||||
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to apply update from %s: %v", sourceType, err)
|
||||
|
||||
// 移除下载失败时恢复备份的逻辑,让用户手动处理
|
||||
if backupPath != "" {
|
||||
s.logger.Info("Update failed, backup is available at: " + backupPath)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.UpdateApplied = true
|
||||
|
||||
// 更新成功后清理备份文件
|
||||
if backupPath != "" {
|
||||
if err := s.cleanupBackup(backupPath); err != nil {
|
||||
s.logger.Error("Failed to cleanup backup", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置中的版本号
|
||||
if err := s.updateConfigVersion(result.LatestVersion); err != nil {
|
||||
s.logger.Error("Failed to update config version", "error", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getUpdateFromSource 从指定源获取更新信息
|
||||
func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType) (*selfupdate.Updater, *selfupdate.Release, bool, error) {
|
||||
var updater *selfupdate.Updater
|
||||
var release *selfupdate.Release
|
||||
var found bool
|
||||
var err error
|
||||
|
||||
switch sourceType {
|
||||
case models.UpdateSourceGithub:
|
||||
updater, err = s.createGithubUpdater()
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
|
||||
}
|
||||
release, found, err = s.checkGithubUpdates(ctx)
|
||||
case models.UpdateSourceGitea:
|
||||
updater, err = s.createGiteaUpdater()
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
|
||||
}
|
||||
release, found, err = s.checkGiteaUpdates(ctx)
|
||||
default:
|
||||
return nil, nil, false, fmt.Errorf("unsupported update source type: %s", sourceType)
|
||||
}
|
||||
|
||||
return updater, release, found, err
|
||||
}
|
||||
|
||||
// RestartApplication 重启应用程序
|
||||
func (s *SelfUpdateService) RestartApplication() error {
|
||||
|
||||
// 获取当前可执行文件路径
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Windows平台需要特殊处理
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
// 获取当前工作目录
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get working directory", "error", err)
|
||||
workDir = filepath.Dir(exe) // 如果获取失败,使用可执行文件所在目录
|
||||
}
|
||||
|
||||
// 创建批处理文件来重启应用程序
|
||||
// 批处理文件会等待当前进程退出,然后启动新进程
|
||||
batchFile := filepath.Join(os.TempDir(), "restart_voidraft.bat")
|
||||
batchContent := fmt.Sprintf(`@echo off
|
||||
timeout /t 1 /nobreak > NUL
|
||||
cd /d "%s"
|
||||
start "" "%s" %s
|
||||
del "%s"
|
||||
`, workDir, exe, strings.Join(os.Args[1:], " "), batchFile)
|
||||
|
||||
s.logger.Info("Creating batch file", "path", batchFile, "content", batchContent)
|
||||
|
||||
// 写入批处理文件
|
||||
err = os.WriteFile(batchFile, []byte(batchContent), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create batch file: %w", err)
|
||||
}
|
||||
|
||||
// 启动批处理文件
|
||||
cmd := exec.Command("cmd.exe", "/C", batchFile)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.Stdin = nil
|
||||
// 分离进程,这样即使父进程退出,批处理文件仍然会继续执行
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start batch file: %w", err)
|
||||
}
|
||||
|
||||
// 立即退出当前进程
|
||||
os.Exit(0)
|
||||
|
||||
return nil // 不会执行到这里
|
||||
}
|
||||
|
||||
// 使用syscall.Exec替换当前进程
|
||||
err = syscall.Exec(exe, os.Args, os.Environ())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to exec: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateConfigVersion 更新配置中的版本号
|
||||
func (s *SelfUpdateService) updateConfigVersion(version string) error {
|
||||
// 使用configService更新配置中的版本号
|
||||
if err := s.configService.Set("updates.version", version); err != nil {
|
||||
return fmt.Errorf("failed to update config version: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackup 创建当前可执行文件的备份
|
||||
func (s *SelfUpdateService) createBackup(executablePath string) (string, error) {
|
||||
backupPath := executablePath + ".backup"
|
||||
|
||||
// 读取原文件
|
||||
data, err := os.ReadFile(executablePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read executable: %w", err)
|
||||
}
|
||||
|
||||
// 写入备份文件
|
||||
err = os.WriteFile(backupPath, data, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
|
||||
return backupPath, nil
|
||||
}
|
||||
|
||||
// cleanupBackup 清理备份文件
|
||||
func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
|
||||
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove backup file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -20,7 +20,7 @@ type ServiceManager struct {
|
||||
keyBindingService *KeyBindingService
|
||||
extensionService *ExtensionService
|
||||
startupService *StartupService
|
||||
updateService *UpdateService
|
||||
selfUpdateService *SelfUpdateService
|
||||
logger *log.LoggerService
|
||||
}
|
||||
|
||||
@@ -62,11 +62,14 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化开机启动服务
|
||||
startupService := NewStartupService(configService, logger)
|
||||
|
||||
// 初始化更新服务
|
||||
updateService := NewUpdateService(configService, logger)
|
||||
// 初始化自我更新服务
|
||||
selfUpdateService, err := NewSelfUpdateService(configService, logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 使用新的配置通知系统设置热键配置变更监听
|
||||
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||
})
|
||||
if err != nil {
|
||||
@@ -92,14 +95,14 @@ func NewServiceManager() *ServiceManager {
|
||||
keyBindingService: keyBindingService,
|
||||
extensionService: extensionService,
|
||||
startupService: startupService,
|
||||
updateService: updateService,
|
||||
selfUpdateService: selfUpdateService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetServices 获取所有wails服务列表
|
||||
func (sm *ServiceManager) GetServices() []application.Service {
|
||||
return []application.Service{
|
||||
services := []application.Service{
|
||||
application.NewService(sm.configService),
|
||||
application.NewService(sm.documentService),
|
||||
application.NewService(sm.migrationService),
|
||||
@@ -110,8 +113,9 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
||||
application.NewService(sm.keyBindingService),
|
||||
application.NewService(sm.extensionService),
|
||||
application.NewService(sm.startupService),
|
||||
application.NewService(sm.updateService),
|
||||
application.NewService(sm.selfUpdateService),
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
// GetHotkeyService 获取热键服务实例
|
||||
@@ -154,7 +158,7 @@ func (sm *ServiceManager) GetExtensionService() *ExtensionService {
|
||||
return sm.extensionService
|
||||
}
|
||||
|
||||
// GetUpdateService 获取更新服务实例
|
||||
func (sm *ServiceManager) GetUpdateService() *UpdateService {
|
||||
return sm.updateService
|
||||
// GetSelfUpdateService 获取自我更新服务实例
|
||||
func (sm *ServiceManager) GetSelfUpdateService() *SelfUpdateService {
|
||||
return sm.selfUpdateService
|
||||
}
|
||||
|
@@ -1,89 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// UpdateCheckResult 更新检查结果
|
||||
type UpdateCheckResult struct {
|
||||
HasUpdate bool `json:"hasUpdate"` // 是否有更新
|
||||
CurrentVer string `json:"currentVer"` // 当前版本
|
||||
LatestVer string `json:"latestVer"` // 最新版本
|
||||
ReleaseNotes string `json:"releaseNotes"` // 发布说明
|
||||
ReleaseURL string `json:"releaseURL"` // 发布页面URL
|
||||
Error string `json:"error"` // 错误信息
|
||||
}
|
||||
|
||||
// UpdateService 更新服务
|
||||
type UpdateService struct {
|
||||
logger *log.LoggerService
|
||||
configService *ConfigService
|
||||
githubClient *github.Client
|
||||
currentVersion string
|
||||
}
|
||||
|
||||
// NewUpdateService 创建更新服务实例
|
||||
func NewUpdateService(configService *ConfigService, logger *log.LoggerService) *UpdateService {
|
||||
config, err := configService.GetConfig()
|
||||
if err != nil {
|
||||
logger.Error("Failed to get config", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
currentVersion := config.Updates.Version
|
||||
if currentVersion == "" {
|
||||
currentVersion = "1.0.0"
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||
githubClient := github.NewClient(httpClient)
|
||||
|
||||
return &UpdateService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
githubClient: githubClient,
|
||||
currentVersion: currentVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckForUpdates 检查更新
|
||||
func (us *UpdateService) CheckForUpdates() UpdateCheckResult {
|
||||
result := UpdateCheckResult{
|
||||
CurrentVer: us.currentVersion,
|
||||
HasUpdate: false,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
release, _, err := us.githubClient.Repositories.GetLatestRelease(ctx, "landaiqing", "voidraft")
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("Failed to check for updates: %v", err)
|
||||
return result
|
||||
}
|
||||
|
||||
if release.GetDraft() || release.GetPrerelease() {
|
||||
result.Error = "Latest release is draft or prerelease"
|
||||
return result
|
||||
}
|
||||
|
||||
latestVer := strings.TrimPrefix(release.GetTagName(), "v")
|
||||
currentVer := strings.TrimPrefix(us.currentVersion, "v")
|
||||
|
||||
result.LatestVer = latestVer
|
||||
result.ReleaseNotes = release.GetBody()
|
||||
result.ReleaseURL = release.GetHTMLURL()
|
||||
|
||||
if latestVer != currentVer && latestVer > currentVer {
|
||||
result.HasUpdate = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
Reference in New Issue
Block a user