mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-12 16:02:48 +08:00
✨ feat(update): 支持 latest 与 dev 更新通道切换
- 新增更新通道持久化与按通道检查 GitHub Release - 关于页支持切换通道并隔离本地更新缓存状态 - 补充多语言文案、Wails 绑定与前后端测试覆盖
This commit is contained in:
@@ -1695,6 +1695,7 @@ function App() {
|
||||
aboutLoading,
|
||||
aboutUpdateStatus,
|
||||
canShowProgressEntry,
|
||||
changeUpdateChannel,
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
formatBytes,
|
||||
@@ -1703,11 +1704,14 @@ function App() {
|
||||
isAboutOpen,
|
||||
isBackgroundProgressForLatestUpdate,
|
||||
isLatestUpdateDownloaded,
|
||||
isUpdateChannelLoading,
|
||||
isUpdateChannelSaving,
|
||||
lastUpdateInfo,
|
||||
markUpdateProgressDismissed,
|
||||
muteLatestUpdate,
|
||||
setIsAboutOpen,
|
||||
showUpdateDownloadProgress,
|
||||
updateChannel,
|
||||
updateDownloadProgress,
|
||||
} = useAppUpdateManager({
|
||||
isMacRuntime,
|
||||
@@ -4998,6 +5002,27 @@ function App() {
|
||||
<div style={{ marginBottom: 6, fontWeight: 600 }}>{t('app.about.field.update_status')}</div>
|
||||
<div style={utilityMutedTextStyle}>{aboutUpdateStatus || t('app.about.update_status.not_checked')}</div>
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<div style={{ marginBottom: 6, fontWeight: 600 }}>{t('app.about.field.update_channel')}</div>
|
||||
<Select
|
||||
value={updateChannel}
|
||||
options={[
|
||||
{ value: 'latest', label: t('app.about.update_channel.latest') },
|
||||
{ value: 'dev', label: t('app.about.update_channel.dev') },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
void changeUpdateChannel(String(value));
|
||||
}}
|
||||
loading={isUpdateChannelLoading}
|
||||
disabled={
|
||||
isUpdateChannelLoading
|
||||
|| isUpdateChannelSaving
|
||||
|| updateDownloadProgress.status === 'start'
|
||||
|| updateDownloadProgress.status === 'downloading'
|
||||
}
|
||||
style={{ width: 220, maxWidth: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
{aboutInfo?.communityUrl ? (
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<div style={{ marginBottom: 6, fontWeight: 600 }}>{t('app.about.field.community')}</div>
|
||||
|
||||
@@ -24,8 +24,10 @@ type BackendAppMock = {
|
||||
CheckForUpdates: ReturnType<typeof vi.fn>;
|
||||
CheckForUpdatesSilently: ReturnType<typeof vi.fn>;
|
||||
DownloadUpdate: ReturnType<typeof vi.fn>;
|
||||
GetUpdateChannel: ReturnType<typeof vi.fn>;
|
||||
InstallUpdateAndRestart: ReturnType<typeof vi.fn>;
|
||||
OpenDownloadedUpdateDirectory: ReturnType<typeof vi.fn>;
|
||||
SetUpdateChannel: ReturnType<typeof vi.fn>;
|
||||
GetAppInfo: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
@@ -33,8 +35,10 @@ const createBackendAppMock = (): BackendAppMock => ({
|
||||
CheckForUpdates: vi.fn(),
|
||||
CheckForUpdatesSilently: vi.fn(),
|
||||
DownloadUpdate: vi.fn(),
|
||||
GetUpdateChannel: vi.fn(async () => ({ success: true, data: { channel: 'latest' } })),
|
||||
InstallUpdateAndRestart: vi.fn(),
|
||||
OpenDownloadedUpdateDirectory: vi.fn(),
|
||||
SetUpdateChannel: vi.fn(async (channel: string) => ({ success: true, data: { channel } })),
|
||||
GetAppInfo: vi.fn(async () => ({ success: true, data: { version: '0.8.1', author: 'Syngnat' } })),
|
||||
});
|
||||
|
||||
@@ -156,4 +160,28 @@ describe('useAppUpdateManager', () => {
|
||||
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
|
||||
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
|
||||
});
|
||||
|
||||
it('switches update channel and re-checks against the selected channel', async () => {
|
||||
backendApp.SetUpdateChannel.mockResolvedValue({ success: true, data: { channel: 'dev' } });
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: false,
|
||||
channel: 'dev',
|
||||
currentVersion: '0.8.1',
|
||||
latestVersion: 'dev-a1b2c3d',
|
||||
},
|
||||
});
|
||||
|
||||
renderHook(false);
|
||||
|
||||
await act(async () => {
|
||||
await hook?.changeUpdateChannel('dev');
|
||||
});
|
||||
|
||||
expect(backendApp.SetUpdateChannel).toHaveBeenCalledWith('dev');
|
||||
expect(backendApp.CheckForUpdates).toHaveBeenCalledTimes(1);
|
||||
expect(hook?.updateChannel).toBe('dev');
|
||||
expect(hook?.lastUpdateInfo?.channel).toBe('dev');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,11 @@ import { resolveAboutDisplayVersion } from '../utils/appVersionDisplay';
|
||||
|
||||
type Translator = (key: string, params?: Record<string, any>) => string;
|
||||
|
||||
export type UpdateChannel = 'latest' | 'dev';
|
||||
|
||||
export type UpdateInfo = {
|
||||
hasUpdate: boolean;
|
||||
channel?: UpdateChannel | string;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
releaseName?: string;
|
||||
@@ -52,8 +55,16 @@ const createEmptyDownloadProgress = () => ({
|
||||
message: '',
|
||||
});
|
||||
|
||||
const normalizeUpdateChannel = (value: unknown): UpdateChannel =>
|
||||
String(value || '').trim().toLowerCase() === 'dev' ? 'dev' : 'latest';
|
||||
|
||||
const buildUpdateKey = (info: Pick<UpdateInfo, 'channel' | 'latestVersion'> | null | undefined): string =>
|
||||
info?.latestVersion
|
||||
? `${normalizeUpdateChannel(info.channel)}:${String(info.latestVersion || '').trim()}`
|
||||
: '';
|
||||
|
||||
export const useAppUpdateManager = ({
|
||||
isMacRuntime,
|
||||
isMacRuntime: _isMacRuntime,
|
||||
runtimeBuildType,
|
||||
t,
|
||||
}: UseAppUpdateManagerOptions) => {
|
||||
@@ -68,6 +79,9 @@ export const useAppUpdateManager = ({
|
||||
const [isAboutOpen, setIsAboutOpen] = useState(false);
|
||||
const isAboutOpenRef = useRef(false);
|
||||
const [aboutLoading, setAboutLoading] = useState(false);
|
||||
const [updateChannel, setUpdateChannelState] = useState<UpdateChannel>('latest');
|
||||
const [isUpdateChannelLoading, setIsUpdateChannelLoading] = useState(false);
|
||||
const [isUpdateChannelSaving, setIsUpdateChannelSaving] = useState(false);
|
||||
const [aboutInfo, setAboutInfo] = useState<{
|
||||
version: string;
|
||||
author: string;
|
||||
@@ -81,13 +95,14 @@ export const useAppUpdateManager = ({
|
||||
const [aboutUpdateStatus, setAboutUpdateStatus] = useState<string>('');
|
||||
const [lastUpdateInfo, setLastUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [updateDownloadProgress, setUpdateDownloadProgress] = useState(createEmptyDownloadProgress);
|
||||
const lastUpdateKey = buildUpdateKey(lastUpdateInfo);
|
||||
|
||||
const formatAboutUpdateStatus = useCallback((info: UpdateInfo | null): string => {
|
||||
if (!info) {
|
||||
return t('app.about.update_status.not_checked');
|
||||
}
|
||||
if (info.hasUpdate) {
|
||||
const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion;
|
||||
const localDownloaded = updateDownloadedVersionRef.current === buildUpdateKey(info);
|
||||
const hasDownloaded = Boolean(info.downloaded) || localDownloaded;
|
||||
return hasDownloaded
|
||||
? t('app.about.update_status.new_version_downloaded', { version: info.latestVersion })
|
||||
@@ -108,9 +123,17 @@ export const useAppUpdateManager = ({
|
||||
return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
|
||||
}, []);
|
||||
|
||||
const resetLocalUpdateArtifacts = useCallback(() => {
|
||||
updateDownloadedVersionRef.current = null;
|
||||
updateInstallTriggeredVersionRef.current = null;
|
||||
updateDownloadMetaRef.current = null;
|
||||
setUpdateDownloadProgress(createEmptyDownloadProgress());
|
||||
}, []);
|
||||
|
||||
const downloadUpdate = useCallback(async (info: UpdateInfo, silent: boolean) => {
|
||||
if (updateDownloadInFlightRef.current) return;
|
||||
if (updateDownloadedVersionRef.current === info.latestVersion) {
|
||||
const targetKey = buildUpdateKey(info);
|
||||
if (updateDownloadedVersionRef.current === targetKey) {
|
||||
if (!silent) {
|
||||
const cachedDownloadPath = updateDownloadMetaRef.current?.downloadPath;
|
||||
void message.info(cachedDownloadPath
|
||||
@@ -125,7 +148,7 @@ export const useAppUpdateManager = ({
|
||||
updateDownloadMetaRef.current = null;
|
||||
setUpdateDownloadProgress({
|
||||
open: true,
|
||||
version: info.latestVersion,
|
||||
version: targetKey,
|
||||
status: 'start',
|
||||
percent: 0,
|
||||
downloaded: 0,
|
||||
@@ -142,7 +165,7 @@ export const useAppUpdateManager = ({
|
||||
if (res?.success) {
|
||||
const resultData = (res?.data || {}) as UpdateDownloadResultData;
|
||||
updateDownloadMetaRef.current = resultData;
|
||||
updateDownloadedVersionRef.current = info.latestVersion;
|
||||
updateDownloadedVersionRef.current = targetKey;
|
||||
setUpdateDownloadProgress((prev) => {
|
||||
const total = prev.total > 0 ? prev.total : (info.assetSize || 0);
|
||||
return { ...prev, status: 'done', percent: 100, downloaded: total, total, message: '', open: false };
|
||||
@@ -151,12 +174,14 @@ export const useAppUpdateManager = ({
|
||||
if (!prev || prev.latestVersion !== info.latestVersion) {
|
||||
return {
|
||||
...info,
|
||||
channel: normalizeUpdateChannel(info.channel),
|
||||
downloaded: true,
|
||||
downloadPath: resultData?.downloadPath || info.downloadPath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
channel: normalizeUpdateChannel(prev.channel || info.channel),
|
||||
downloaded: true,
|
||||
downloadPath: resultData?.downloadPath || prev.downloadPath || info.downloadPath,
|
||||
};
|
||||
@@ -166,7 +191,7 @@ export const useAppUpdateManager = ({
|
||||
} else {
|
||||
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
|
||||
}
|
||||
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, downloaded: true }));
|
||||
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, channel: normalizeUpdateChannel(info.channel), downloaded: true }));
|
||||
} else {
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
@@ -175,7 +200,7 @@ export const useAppUpdateManager = ({
|
||||
}));
|
||||
void message.error({ content: t('app.about.message.download_failed_with_error', { error: res?.message || t('common.unknown') }), duration: 4 });
|
||||
}
|
||||
}, [formatAboutUpdateStatus, isMacRuntime, t]);
|
||||
}, [formatAboutUpdateStatus, t]);
|
||||
|
||||
const showUpdateDownloadProgress = useCallback(() => {
|
||||
setUpdateDownloadProgress((prev) => {
|
||||
@@ -190,21 +215,21 @@ export const useAppUpdateManager = ({
|
||||
|
||||
const isLatestUpdateDownloaded = Boolean(lastUpdateInfo?.hasUpdate) && (
|
||||
Boolean(lastUpdateInfo?.downloaded)
|
||||
|| (Boolean(lastUpdateInfo?.latestVersion) && updateDownloadedVersionRef.current === lastUpdateInfo?.latestVersion)
|
||||
|| (Boolean(lastUpdateKey) && updateDownloadedVersionRef.current === lastUpdateKey)
|
||||
);
|
||||
const isBackgroundProgressForLatestUpdate = Boolean(lastUpdateInfo?.hasUpdate)
|
||||
&& Boolean(lastUpdateInfo?.latestVersion)
|
||||
&& updateDownloadProgress.version === lastUpdateInfo?.latestVersion
|
||||
&& Boolean(lastUpdateKey)
|
||||
&& updateDownloadProgress.version === lastUpdateKey
|
||||
&& (updateDownloadProgress.status === 'start'
|
||||
|| updateDownloadProgress.status === 'downloading'
|
||||
|| updateDownloadProgress.status === 'done'
|
||||
|| updateDownloadProgress.status === 'error');
|
||||
const canShowProgressEntry = (isLatestUpdateDownloaded || isBackgroundProgressForLatestUpdate)
|
||||
&& updateInstallTriggeredVersionRef.current !== (lastUpdateInfo?.latestVersion || null);
|
||||
&& updateInstallTriggeredVersionRef.current !== (lastUpdateKey || null);
|
||||
|
||||
const handleInstallFromProgress = useCallback(async () => {
|
||||
const canInstall = updateDownloadProgress.status === 'done'
|
||||
|| (Boolean(lastUpdateInfo?.hasUpdate) && (Boolean(lastUpdateInfo?.downloaded) || updateDownloadedVersionRef.current === lastUpdateInfo?.latestVersion));
|
||||
|| (Boolean(lastUpdateInfo?.hasUpdate) && (Boolean(lastUpdateInfo?.downloaded) || updateDownloadedVersionRef.current === lastUpdateKey));
|
||||
if (!canInstall) {
|
||||
return;
|
||||
}
|
||||
@@ -213,9 +238,9 @@ export const useAppUpdateManager = ({
|
||||
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
|
||||
return;
|
||||
}
|
||||
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
|
||||
updateInstallTriggeredVersionRef.current = lastUpdateKey || null;
|
||||
hideUpdateDownloadProgress();
|
||||
}, [hideUpdateDownloadProgress, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
|
||||
}, [hideUpdateDownloadProgress, lastUpdateInfo, lastUpdateKey, t, updateDownloadProgress.status]);
|
||||
|
||||
const checkForUpdates = useCallback(async (silent: boolean) => {
|
||||
if (updateCheckInFlightRef.current) return;
|
||||
@@ -237,19 +262,24 @@ export const useAppUpdateManager = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
const info: UpdateInfo = res.data;
|
||||
const info: UpdateInfo = {
|
||||
...(res.data || {}),
|
||||
channel: normalizeUpdateChannel(res?.data?.channel),
|
||||
};
|
||||
if (!info) return;
|
||||
setUpdateChannelState(normalizeUpdateChannel(info.channel));
|
||||
const aboutOpen = isAboutOpenRef.current;
|
||||
if (info.hasUpdate) {
|
||||
if (!info.downloaded && updateDownloadedVersionRef.current === info.latestVersion) {
|
||||
const infoKey = buildUpdateKey(info);
|
||||
if (!info.downloaded && updateDownloadedVersionRef.current === infoKey) {
|
||||
updateDownloadedVersionRef.current = null;
|
||||
updateDownloadMetaRef.current = null;
|
||||
}
|
||||
const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion;
|
||||
const localDownloaded = updateDownloadedVersionRef.current === infoKey;
|
||||
const hasDownloaded = Boolean(info.downloaded) || localDownloaded;
|
||||
if (hasDownloaded) {
|
||||
const downloadPath = info.downloadPath || updateDownloadMetaRef.current?.downloadPath || '';
|
||||
updateDownloadedVersionRef.current = info.latestVersion;
|
||||
updateDownloadedVersionRef.current = infoKey;
|
||||
updateDownloadMetaRef.current = {
|
||||
...(updateDownloadMetaRef.current || {}),
|
||||
info,
|
||||
@@ -262,8 +292,8 @@ export const useAppUpdateManager = ({
|
||||
const total = info.assetSize || prev.total || 0;
|
||||
return {
|
||||
...prev,
|
||||
open: prev.open && prev.version === info.latestVersion,
|
||||
version: info.latestVersion,
|
||||
open: prev.open && prev.version === infoKey,
|
||||
version: infoKey,
|
||||
status: 'done',
|
||||
percent: 100,
|
||||
downloaded: total,
|
||||
@@ -277,7 +307,7 @@ export const useAppUpdateManager = ({
|
||||
downloadPath: downloadPath || undefined,
|
||||
});
|
||||
} else {
|
||||
if (updateDownloadedVersionRef.current !== info.latestVersion) {
|
||||
if (updateDownloadedVersionRef.current !== infoKey) {
|
||||
updateDownloadMetaRef.current = null;
|
||||
}
|
||||
setUpdateDownloadProgress((prev) => {
|
||||
@@ -287,7 +317,7 @@ export const useAppUpdateManager = ({
|
||||
return {
|
||||
...prev,
|
||||
open: false,
|
||||
version: info.latestVersion,
|
||||
version: infoKey,
|
||||
status: 'idle',
|
||||
percent: 0,
|
||||
downloaded: 0,
|
||||
@@ -305,8 +335,8 @@ export const useAppUpdateManager = ({
|
||||
if (silent && aboutOpen) {
|
||||
setAboutUpdateStatus(statusText);
|
||||
}
|
||||
if (silent && !aboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) {
|
||||
updateNotifiedVersionRef.current = info.latestVersion;
|
||||
if (silent && !aboutOpen && updateMutedVersionRef.current !== infoKey && updateNotifiedVersionRef.current !== infoKey) {
|
||||
updateNotifiedVersionRef.current = infoKey;
|
||||
setIsAboutOpen(true);
|
||||
}
|
||||
} else if (!silent) {
|
||||
@@ -346,12 +376,63 @@ export const useAppUpdateManager = ({
|
||||
setAboutLoading(false);
|
||||
}, [t]);
|
||||
|
||||
const loadUpdateChannel = useCallback(async () => {
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
if (typeof backendApp?.GetUpdateChannel !== 'function') {
|
||||
return;
|
||||
}
|
||||
setIsUpdateChannelLoading(true);
|
||||
try {
|
||||
const res = await backendApp.GetUpdateChannel();
|
||||
if (res?.success) {
|
||||
setUpdateChannelState(normalizeUpdateChannel(res?.data?.channel));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Wails API: GetUpdateChannel unavailable', e);
|
||||
} finally {
|
||||
setIsUpdateChannelLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changeUpdateChannel = useCallback(async (nextChannel: UpdateChannel | string) => {
|
||||
const normalizedChannel = normalizeUpdateChannel(nextChannel);
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
if (typeof backendApp?.SetUpdateChannel !== 'function') {
|
||||
setUpdateChannelState(normalizedChannel);
|
||||
resetLocalUpdateArtifacts();
|
||||
setLastUpdateInfo(null);
|
||||
setAboutUpdateStatus(t('app.about.update_status.not_checked'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdateChannelSaving(true);
|
||||
try {
|
||||
const res = await backendApp.SetUpdateChannel(normalizedChannel);
|
||||
if (!res?.success) {
|
||||
void message.error(t('app.about.message.channel_switch_failed_with_error', { error: res?.message || t('common.unknown') }));
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveChannel = normalizeUpdateChannel(res?.data?.channel || normalizedChannel);
|
||||
setUpdateChannelState(effectiveChannel);
|
||||
resetLocalUpdateArtifacts();
|
||||
setLastUpdateInfo(null);
|
||||
setAboutUpdateStatus(t('app.about.update_status.not_checked'));
|
||||
await checkForUpdates(false);
|
||||
} catch (e: any) {
|
||||
const error = e?.message || t('common.unknown');
|
||||
void message.error(t('app.about.message.channel_switch_failed_with_error', { error }));
|
||||
} finally {
|
||||
setIsUpdateChannelSaving(false);
|
||||
}
|
||||
}, [checkForUpdates, resetLocalUpdateArtifacts, t]);
|
||||
|
||||
const muteLatestUpdate = useCallback(() => {
|
||||
if (lastUpdateInfo?.latestVersion) {
|
||||
updateMutedVersionRef.current = lastUpdateInfo.latestVersion;
|
||||
if (lastUpdateKey) {
|
||||
updateMutedVersionRef.current = lastUpdateKey;
|
||||
}
|
||||
setIsAboutOpen(false);
|
||||
}, [lastUpdateInfo?.latestVersion]);
|
||||
}, [lastUpdateKey]);
|
||||
|
||||
const markUpdateProgressDismissed = useCallback(() => {
|
||||
updateUserDismissedRef.current = true;
|
||||
@@ -368,6 +449,10 @@ export const useAppUpdateManager = ({
|
||||
}
|
||||
}, [formatAboutUpdateStatus, isAboutOpen, lastUpdateInfo, loadAboutInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUpdateChannel();
|
||||
}, [loadUpdateChannel]);
|
||||
|
||||
useEffect(() => {
|
||||
const startupTimer = window.setTimeout(() => {
|
||||
void checkForUpdates(true);
|
||||
@@ -421,6 +506,7 @@ export const useAppUpdateManager = ({
|
||||
aboutLoading,
|
||||
aboutUpdateStatus,
|
||||
canShowProgressEntry,
|
||||
changeUpdateChannel,
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
formatBytes,
|
||||
@@ -429,11 +515,14 @@ export const useAppUpdateManager = ({
|
||||
isAboutOpen,
|
||||
isBackgroundProgressForLatestUpdate,
|
||||
isLatestUpdateDownloaded,
|
||||
isUpdateChannelLoading,
|
||||
isUpdateChannelSaving,
|
||||
lastUpdateInfo,
|
||||
markUpdateProgressDismissed,
|
||||
muteLatestUpdate,
|
||||
setIsAboutOpen,
|
||||
showUpdateDownloadProgress,
|
||||
updateChannel,
|
||||
updateDownloadProgress,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ if (
|
||||
];
|
||||
let mockSkills: any[] = [];
|
||||
let mockGlobalProxy: any = { enabled: false, type: 'socks5', host: '', port: 1080, user: '', password: '', hasPassword: false };
|
||||
let mockUpdateChannel: 'latest' | 'dev' = 'latest';
|
||||
let mockDataRootInfo: any = {
|
||||
path: 'C:/mock/.gonavi',
|
||||
defaultPath: 'C:/mock/.gonavi',
|
||||
@@ -343,10 +344,27 @@ if (
|
||||
bindingStatus: 'active',
|
||||
});
|
||||
},
|
||||
GetAppInfo: async () => ({}),
|
||||
GetAppInfo: async () => ({ success: true, data: { version: '0.0.0', author: 'GoNavi' } }),
|
||||
GetDataRootDirectoryInfo: async () => ({ success: true, data: cloneBrowserMockValue(mockDataRootInfo) }),
|
||||
CheckForUpdates: async () => ({ success: false }),
|
||||
CheckForUpdatesSilently: async () => ({ success: false }),
|
||||
CheckForUpdates: async () => ({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: false,
|
||||
channel: mockUpdateChannel,
|
||||
currentVersion: '0.0.0',
|
||||
latestVersion: mockUpdateChannel === 'dev' ? 'dev-browser-mock' : '0.0.0',
|
||||
},
|
||||
}),
|
||||
CheckForUpdatesSilently: async () => ({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: false,
|
||||
channel: mockUpdateChannel,
|
||||
currentVersion: '0.0.0',
|
||||
latestVersion: mockUpdateChannel === 'dev' ? 'dev-browser-mock' : '0.0.0',
|
||||
},
|
||||
}),
|
||||
GetUpdateChannel: async () => ({ success: true, data: { channel: mockUpdateChannel } }),
|
||||
OpenDownloadedUpdateDirectory: async () => ({ success: false }),
|
||||
OpenDriverDownloadDirectory: async (path: string) => ({ success: true, data: { path } }),
|
||||
OpenDataRootDirectory: async () => ({ success: true }),
|
||||
@@ -408,6 +426,10 @@ if (
|
||||
ExportConnectionsPackage: async (_options?: { includeSecrets?: boolean; filePassword?: string }) => ({ success: false, message: t('app.browser_mock.export_connection_package_unsupported') }),
|
||||
ExportData: async () => ({ success: false }),
|
||||
GetGlobalProxyConfig: async () => ({ success: true, data: cloneBrowserMockValue(mockGlobalProxy) }),
|
||||
SetUpdateChannel: async (channel: string) => {
|
||||
mockUpdateChannel = String(channel || '').trim().toLowerCase() === 'dev' ? 'dev' : 'latest';
|
||||
return { success: true, data: { channel: mockUpdateChannel } };
|
||||
},
|
||||
SaveGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||||
ImportLegacyGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||||
SelectDataRootDirectory: async (currentPath: string) => ({ success: true, data: { ...mockDataRootInfo, path: currentPath || mockDataRootInfo.path } }),
|
||||
|
||||
6
frontend/wailsjs/go/app/App.d.ts
vendored
6
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -170,6 +170,8 @@ export function GetSlowQueries(arg1:connection.ConnectionConfig,arg2:string,arg3
|
||||
|
||||
export function GetUnboundSavedQueries():Promise<Array<connection.SavedQuery>>;
|
||||
|
||||
export function GetUpdateChannel():Promise<connection.QueryResult>;
|
||||
|
||||
export function ImportConfigFile():Promise<connection.QueryResult>;
|
||||
|
||||
export function ImportConnectionsPayload(arg1:string,arg2:string):Promise<Array<connection.SavedConnectionView>>;
|
||||
@@ -352,12 +354,16 @@ export function SelectDriverPackageFile(arg1:string):Promise<connection.QueryRes
|
||||
|
||||
export function SelectSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SelectSQLFileForExecution():Promise<connection.QueryResult>;
|
||||
|
||||
export function SelectSSHKeyFile(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SetLanguage(arg1:string):Promise<void>;
|
||||
|
||||
export function SetMacNativeWindowControls(arg1:boolean):Promise<void>;
|
||||
|
||||
export function SetUpdateChannel(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SetWindowTranslucency(arg1:number,arg2:number):Promise<void>;
|
||||
|
||||
export function Shutdown():Promise<void>;
|
||||
|
||||
@@ -330,6 +330,10 @@ export function GetUnboundSavedQueries() {
|
||||
return window['go']['app']['App']['GetUnboundSavedQueries']();
|
||||
}
|
||||
|
||||
export function GetUpdateChannel() {
|
||||
return window['go']['app']['App']['GetUpdateChannel']();
|
||||
}
|
||||
|
||||
export function ImportConfigFile() {
|
||||
return window['go']['app']['App']['ImportConfigFile']();
|
||||
}
|
||||
@@ -694,6 +698,10 @@ export function SelectSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['SelectSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
export function SelectSQLFileForExecution() {
|
||||
return window['go']['app']['App']['SelectSQLFileForExecution']();
|
||||
}
|
||||
|
||||
export function SelectSSHKeyFile(arg1) {
|
||||
return window['go']['app']['App']['SelectSSHKeyFile'](arg1);
|
||||
}
|
||||
@@ -706,6 +714,10 @@ export function SetMacNativeWindowControls(arg1) {
|
||||
return window['go']['app']['App']['SetMacNativeWindowControls'](arg1);
|
||||
}
|
||||
|
||||
export function SetUpdateChannel(arg1) {
|
||||
return window['go']['app']['App']['SetUpdateChannel'](arg1);
|
||||
}
|
||||
|
||||
export function SetWindowTranslucency(arg1, arg2) {
|
||||
return window['go']['app']['App']['SetWindowTranslucency'](arg1, arg2);
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -23,6 +23,7 @@ require (
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
github.com/segmentio/kafka-go v0.4.51
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/taosdata/driver-go/v3 v3.7.8
|
||||
github.com/trinodb/trino-go-client v0.333.0
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
@@ -59,7 +60,6 @@ require (
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/tidwall/gjson v1.14.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
|
||||
@@ -27,13 +27,15 @@ import (
|
||||
|
||||
const (
|
||||
updateRepo = "Syngnat/GoNavi"
|
||||
updateAPIURL = "https://api.github.com/repos/" + updateRepo + "/releases/latest"
|
||||
updateLatestAPIURL = "https://api.github.com/repos/" + updateRepo + "/releases/latest"
|
||||
updateDevAPIURL = "https://api.github.com/repos/" + updateRepo + "/releases/tags/" + updateDevReleaseTag
|
||||
updateChecksumAsset = "SHA256SUMS"
|
||||
updateDownloadProgressEvent = "update:download-progress"
|
||||
)
|
||||
|
||||
var (
|
||||
updateFetchLatestRelease = fetchLatestRelease
|
||||
updateFetchDevRelease = fetchDevRelease
|
||||
updateFetchReleaseSHA256 = fetchReleaseSHA256
|
||||
updateLogCheckError = func(err error) { logger.Error(err, "检查更新失败") }
|
||||
)
|
||||
@@ -46,6 +48,7 @@ type updateState struct {
|
||||
|
||||
type UpdateInfo struct {
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
Channel string `json:"channel"`
|
||||
CurrentVersion string `json:"currentVersion"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
ReleaseName string `json:"releaseName"`
|
||||
@@ -86,6 +89,7 @@ type updateDownloadProgressPayload struct {
|
||||
}
|
||||
|
||||
type stagedUpdate struct {
|
||||
Channel updateChannel
|
||||
Version string
|
||||
AssetName string
|
||||
FilePath string
|
||||
@@ -138,7 +142,8 @@ func (a *App) CheckForUpdatesSilently() connection.QueryResult {
|
||||
}
|
||||
|
||||
func (a *App) checkForUpdates(logFailure bool) connection.QueryResult {
|
||||
info, err := fetchLatestUpdateInfo()
|
||||
channel := a.currentUpdateChannel()
|
||||
info, err := fetchLatestUpdateInfo(channel)
|
||||
if err != nil {
|
||||
if logFailure {
|
||||
updateLogCheckError(err)
|
||||
@@ -157,7 +162,7 @@ func (a *App) checkForUpdates(logFailure bool) connection.QueryResult {
|
||||
info.Downloaded = true
|
||||
info.DownloadPath = reusable.FilePath
|
||||
currentStaged = reusable
|
||||
} else if currentStaged != nil && currentStaged.Version != info.LatestVersion {
|
||||
} else if currentStaged != nil && (currentStaged.Version != info.LatestVersion || currentStaged.Channel != updateChannel(info.Channel)) {
|
||||
currentStaged = nil
|
||||
}
|
||||
} else {
|
||||
@@ -336,7 +341,7 @@ func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
}
|
||||
|
||||
// 使用版本号命名的工作目录,便于识别和调试
|
||||
stagedDir := filepath.Join(workspaceDir, fmt.Sprintf(".gonavi-update-%s-%s", stdRuntime.GOOS, info.LatestVersion))
|
||||
stagedDir := filepath.Join(workspaceDir, buildUpdateStageDirName(info.Channel, info.LatestVersion))
|
||||
// 清理可能残留的旧目录(上次下载失败后未清理)
|
||||
// Windows 上文件可能被杀毒软件/索引服务占用,需要重试
|
||||
for retry := 0; retry < 5; retry++ {
|
||||
@@ -348,7 +353,7 @@ func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
time.Sleep(time.Duration(retry+1) * 500 * time.Millisecond)
|
||||
} else {
|
||||
// 最后一次仍然失败,换一个带时间戳的目录名避免冲突
|
||||
stagedDir = filepath.Join(workspaceDir, fmt.Sprintf(".gonavi-update-%s-%s-%d", stdRuntime.GOOS, info.LatestVersion, time.Now().UnixNano()))
|
||||
stagedDir = filepath.Join(workspaceDir, fmt.Sprintf("%s-%d", buildUpdateStageDirName(info.Channel, info.LatestVersion), time.Now().UnixNano()))
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
|
||||
@@ -390,6 +395,7 @@ func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
}
|
||||
|
||||
staged := &stagedUpdate{
|
||||
Channel: updateChannel(info.Channel),
|
||||
Version: info.LatestVersion,
|
||||
AssetName: info.AssetName,
|
||||
FilePath: assetPath,
|
||||
@@ -406,22 +412,31 @@ func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult {
|
||||
return connection.QueryResult{Success: true, Message: a.appText("app.update.backend.message.package_downloaded", nil), Data: buildUpdateDownloadResult(info, staged)}
|
||||
}
|
||||
|
||||
func fetchLatestUpdateInfo() (UpdateInfo, error) {
|
||||
release, err := updateFetchLatestRelease()
|
||||
func fetchLatestUpdateInfo(channel updateChannel) (UpdateInfo, error) {
|
||||
if channel != updateChannelDev {
|
||||
channel = updateChannelLatest
|
||||
}
|
||||
release, err := fetchReleaseForChannel(channel)
|
||||
if err != nil {
|
||||
return UpdateInfo{}, err
|
||||
}
|
||||
|
||||
currentVersion := getCurrentVersion()
|
||||
latestVersion := normalizeVersion(release.TagName)
|
||||
latestVersion := resolveReleaseVersion(channel, release)
|
||||
if latestVersion == "" {
|
||||
return UpdateInfo{}, localizedUpdateError{key: "app.update.backend.error.latest_version_unparseable"}
|
||||
}
|
||||
|
||||
hasUpdate := compareVersion(currentVersion, latestVersion) < 0
|
||||
hasUpdate := false
|
||||
if channel == updateChannelDev {
|
||||
hasUpdate = normalizeVersion(currentVersion) != latestVersion
|
||||
} else {
|
||||
hasUpdate = compareVersion(currentVersion, latestVersion) < 0
|
||||
}
|
||||
if !hasUpdate {
|
||||
return UpdateInfo{
|
||||
HasUpdate: false,
|
||||
Channel: string(channel),
|
||||
CurrentVersion: currentVersion,
|
||||
LatestVersion: latestVersion,
|
||||
ReleaseName: release.Name,
|
||||
@@ -430,7 +445,7 @@ func fetchLatestUpdateInfo() (UpdateInfo, error) {
|
||||
}
|
||||
|
||||
assetVersion := strings.TrimSpace(release.TagName)
|
||||
if assetVersion == "" {
|
||||
if assetVersion == "" || strings.EqualFold(normalizeVersion(assetVersion), updateDevReleaseTag) {
|
||||
assetVersion = latestVersion
|
||||
}
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, assetVersion)
|
||||
@@ -452,6 +467,7 @@ func fetchLatestUpdateInfo() (UpdateInfo, error) {
|
||||
}
|
||||
return UpdateInfo{
|
||||
HasUpdate: hasUpdate,
|
||||
Channel: string(channel),
|
||||
CurrentVersion: currentVersion,
|
||||
LatestVersion: latestVersion,
|
||||
ReleaseName: release.Name,
|
||||
@@ -463,6 +479,13 @@ func fetchLatestUpdateInfo() (UpdateInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fetchReleaseForChannel(channel updateChannel) (*githubRelease, error) {
|
||||
if channel == updateChannelDev {
|
||||
return updateFetchDevRelease()
|
||||
}
|
||||
return updateFetchLatestRelease()
|
||||
}
|
||||
|
||||
func swapUpdateFetchLatestRelease(next func() (*githubRelease, error)) func() {
|
||||
original := updateFetchLatestRelease
|
||||
updateFetchLatestRelease = next
|
||||
@@ -471,6 +494,14 @@ func swapUpdateFetchLatestRelease(next func() (*githubRelease, error)) func() {
|
||||
}
|
||||
}
|
||||
|
||||
func swapUpdateFetchDevRelease(next func() (*githubRelease, error)) func() {
|
||||
original := updateFetchDevRelease
|
||||
updateFetchDevRelease = next
|
||||
return func() {
|
||||
updateFetchDevRelease = original
|
||||
}
|
||||
}
|
||||
|
||||
func swapUpdateFetchReleaseSHA256(next func([]githubAsset) (map[string]string, error)) func() {
|
||||
original := updateFetchReleaseSHA256
|
||||
updateFetchReleaseSHA256 = next
|
||||
@@ -499,8 +530,16 @@ func getCurrentAuthor() string {
|
||||
}
|
||||
|
||||
func fetchLatestRelease() (*githubRelease, error) {
|
||||
return fetchReleaseByURL(updateLatestAPIURL)
|
||||
}
|
||||
|
||||
func fetchDevRelease() (*githubRelease, error) {
|
||||
return fetchReleaseByURL(updateDevAPIURL)
|
||||
}
|
||||
|
||||
func fetchReleaseByURL(apiURL string) (*githubRelease, error) {
|
||||
client := newHTTPClientWithGlobalProxy(15 * time.Second)
|
||||
req, err := http.NewRequest(http.MethodGet, updateAPIURL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -806,6 +845,84 @@ func buildUpdateInstallLogPath(baseDir string) string {
|
||||
return filepath.Join(logDir, fmt.Sprintf("gonavi-update-%s-%d.log", platform, time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
func buildUpdateStageDirName(channel string, version string) string {
|
||||
normalizedChannel, err := normalizeUpdateChannel(channel)
|
||||
if err != nil {
|
||||
normalizedChannel = updateChannelLatest
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
".gonavi-update-%s-%s-%s",
|
||||
stdRuntime.GOOS,
|
||||
sanitizeVersionForPath(string(normalizedChannel)),
|
||||
sanitizeVersionForPath(version),
|
||||
)
|
||||
}
|
||||
|
||||
func resolveReleaseVersion(channel updateChannel, release *githubRelease) string {
|
||||
if release == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
tagVersion := normalizeVersion(release.TagName)
|
||||
if channel != updateChannelDev && tagVersion != "" && !strings.EqualFold(tagVersion, updateDevReleaseTag) {
|
||||
return tagVersion
|
||||
}
|
||||
|
||||
if nameVersion := extractVersionFromReleaseName(release.Name); nameVersion != "" {
|
||||
return nameVersion
|
||||
}
|
||||
if assetVersion := extractVersionFromReleaseAssets(release.Assets); assetVersion != "" {
|
||||
return assetVersion
|
||||
}
|
||||
if tagVersion != "" && !strings.EqualFold(tagVersion, updateDevReleaseTag) {
|
||||
return tagVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractVersionFromReleaseName(name string) string {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(trimmed), "dev-") {
|
||||
return normalizeVersion(trimmed)
|
||||
}
|
||||
|
||||
if left := strings.LastIndex(trimmed, "("); left >= 0 && strings.HasSuffix(trimmed, ")") {
|
||||
candidate := strings.TrimSpace(trimmed[left+1 : len(trimmed)-1])
|
||||
if candidate != "" {
|
||||
return normalizeVersion(candidate)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractVersionFromReleaseAssets(assets []githubAsset) string {
|
||||
const assetPrefix = "GoNavi-"
|
||||
osMarkers := []string{"-Windows-", "-MacOS-", "-Linux-"}
|
||||
|
||||
for _, asset := range assets {
|
||||
name := strings.TrimSpace(asset.Name)
|
||||
if !strings.HasPrefix(name, assetPrefix) {
|
||||
continue
|
||||
}
|
||||
rest := strings.TrimPrefix(name, assetPrefix)
|
||||
for _, marker := range osMarkers {
|
||||
index := strings.Index(rest, marker)
|
||||
if index <= 0 {
|
||||
continue
|
||||
}
|
||||
candidate := normalizeVersion(rest[:index])
|
||||
if candidate != "" {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeVersionForPath(version string) string {
|
||||
trimmed := strings.TrimSpace(version)
|
||||
if trimmed == "" {
|
||||
@@ -877,19 +994,30 @@ func isExistingDownloadedAsset(filePath string, expectedSize int64) bool {
|
||||
}
|
||||
|
||||
func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *stagedUpdate {
|
||||
channel, err := normalizeUpdateChannel(info.Channel)
|
||||
if err != nil {
|
||||
channel = updateChannelLatest
|
||||
}
|
||||
version := strings.TrimSpace(info.LatestVersion)
|
||||
assetName := strings.TrimSpace(info.AssetName)
|
||||
if version == "" || assetName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if current != nil && strings.TrimSpace(current.Version) == version {
|
||||
currentPath := strings.TrimSpace(current.FilePath)
|
||||
if isExistingDownloadedAsset(currentPath, info.AssetSize) {
|
||||
if strings.TrimSpace(current.InstallLogPath) == "" {
|
||||
current.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(currentPath))
|
||||
if current != nil {
|
||||
currentChannel := current.Channel
|
||||
if currentChannel == "" {
|
||||
currentChannel = updateChannelLatest
|
||||
}
|
||||
if currentChannel == channel && strings.TrimSpace(current.Version) == version {
|
||||
currentPath := strings.TrimSpace(current.FilePath)
|
||||
if isExistingDownloadedAsset(currentPath, info.AssetSize) {
|
||||
if strings.TrimSpace(current.InstallLogPath) == "" {
|
||||
current.InstallLogPath = buildUpdateInstallLogPath(filepath.Dir(currentPath))
|
||||
}
|
||||
current.Channel = channel
|
||||
return current
|
||||
}
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +1026,10 @@ func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *staged
|
||||
stagedDir string
|
||||
assetPath string
|
||||
}
|
||||
stagedDirName := fmt.Sprintf(".gonavi-update-%s-%s", stdRuntime.GOOS, version)
|
||||
stagedDirNames := []string{
|
||||
buildUpdateStageDirName(string(channel), version),
|
||||
fmt.Sprintf(".gonavi-update-%s-%s", stdRuntime.GOOS, version),
|
||||
}
|
||||
workspaceCandidates := []string{
|
||||
resolveUpdateWorkspaceDir(version),
|
||||
resolveLegacyUpdateWorkspaceDir(),
|
||||
@@ -915,20 +1046,22 @@ func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *staged
|
||||
}
|
||||
seenWorkspace[workspaceDir] = struct{}{}
|
||||
|
||||
stagedDir := filepath.Join(workspaceDir, stagedDirName)
|
||||
assetPath := resolveUpdateAssetPath(workspaceDir, stagedDir, assetName)
|
||||
candidates = append(candidates, pathCandidate{
|
||||
workspaceDir: workspaceDir,
|
||||
stagedDir: stagedDir,
|
||||
assetPath: assetPath,
|
||||
})
|
||||
legacyAssetPath := filepath.Join(stagedDir, assetName)
|
||||
if legacyAssetPath != assetPath {
|
||||
for _, stagedDirName := range stagedDirNames {
|
||||
stagedDir := filepath.Join(workspaceDir, stagedDirName)
|
||||
assetPath := resolveUpdateAssetPath(workspaceDir, stagedDir, assetName)
|
||||
candidates = append(candidates, pathCandidate{
|
||||
workspaceDir: workspaceDir,
|
||||
stagedDir: stagedDir,
|
||||
assetPath: legacyAssetPath,
|
||||
assetPath: assetPath,
|
||||
})
|
||||
legacyAssetPath := filepath.Join(stagedDir, assetName)
|
||||
if legacyAssetPath != assetPath {
|
||||
candidates = append(candidates, pathCandidate{
|
||||
workspaceDir: workspaceDir,
|
||||
stagedDir: stagedDir,
|
||||
assetPath: legacyAssetPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,6 +1070,7 @@ func resolveReusableStagedUpdate(info UpdateInfo, current *stagedUpdate) *staged
|
||||
continue
|
||||
}
|
||||
return &stagedUpdate{
|
||||
Channel: channel,
|
||||
Version: version,
|
||||
AssetName: assetName,
|
||||
FilePath: candidate.assetPath,
|
||||
|
||||
@@ -2,6 +2,8 @@ package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
stdRuntime "runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -44,7 +46,7 @@ func TestFetchLatestUpdateInfoSkipsChecksumWhenCurrentVersionIsAlreadyLatest(t *
|
||||
})
|
||||
defer restoreChecksum()
|
||||
|
||||
info, err := fetchLatestUpdateInfo()
|
||||
info, err := fetchLatestUpdateInfo(updateChannelLatest)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
@@ -99,7 +101,7 @@ func TestFetchLatestUpdateInfoFetchesChecksumWhenUpdateIsAvailable(t *testing.T)
|
||||
})
|
||||
defer restoreChecksum()
|
||||
|
||||
info, err := fetchLatestUpdateInfo()
|
||||
info, err := fetchLatestUpdateInfo(updateChannelLatest)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
@@ -160,6 +162,171 @@ func TestCheckForUpdatesSilentlySkipsFailureLogs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoForDevChannelUsesReleaseBuildVersion(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "dev-a1b2c3d")
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetName returned error: %v", err)
|
||||
}
|
||||
|
||||
originalVersion := AppVersion
|
||||
AppVersion = "0.6.5"
|
||||
defer func() {
|
||||
AppVersion = originalVersion
|
||||
}()
|
||||
|
||||
restoreRelease := swapUpdateFetchDevRelease(func() (*githubRelease, error) {
|
||||
return &githubRelease{
|
||||
TagName: "dev-latest",
|
||||
Name: "🧪 Dev Build (dev-a1b2c3d)",
|
||||
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/dev-latest",
|
||||
Assets: []githubAsset{
|
||||
{
|
||||
Name: assetName,
|
||||
BrowserDownloadURL: "https://example.com/" + assetName,
|
||||
Size: 8192,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restoreRelease()
|
||||
|
||||
checksumCalled := false
|
||||
restoreChecksum := swapUpdateFetchReleaseSHA256(func([]githubAsset) (map[string]string, error) {
|
||||
checksumCalled = true
|
||||
return map[string]string{
|
||||
assetName: "def456",
|
||||
}, nil
|
||||
})
|
||||
defer restoreChecksum()
|
||||
|
||||
info, err := fetchLatestUpdateInfo(updateChannelDev)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
if !checksumCalled {
|
||||
t.Fatal("expected dev channel update check to fetch SHA256 when build version differs")
|
||||
}
|
||||
if !info.HasUpdate {
|
||||
t.Fatalf("expected HasUpdate=true, got %#v", info)
|
||||
}
|
||||
if info.Channel != string(updateChannelDev) {
|
||||
t.Fatalf("expected dev channel, got %#v", info)
|
||||
}
|
||||
if info.LatestVersion != "dev-a1b2c3d" {
|
||||
t.Fatalf("expected dev build version from release metadata, got %#v", info)
|
||||
}
|
||||
if info.AssetName != assetName || info.SHA256 != "def456" {
|
||||
t.Fatalf("unexpected dev update info: %#v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoForDevChannelSkipsChecksumWhenBuildMatches(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "dev-a1b2c3d")
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetName returned error: %v", err)
|
||||
}
|
||||
|
||||
originalVersion := AppVersion
|
||||
AppVersion = "dev-a1b2c3d"
|
||||
defer func() {
|
||||
AppVersion = originalVersion
|
||||
}()
|
||||
|
||||
restoreRelease := swapUpdateFetchDevRelease(func() (*githubRelease, error) {
|
||||
return &githubRelease{
|
||||
TagName: "dev-latest",
|
||||
Name: "🧪 Dev Build (dev-a1b2c3d)",
|
||||
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/dev-latest",
|
||||
Assets: []githubAsset{
|
||||
{
|
||||
Name: assetName,
|
||||
BrowserDownloadURL: "https://example.com/" + assetName,
|
||||
Size: 2048,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restoreRelease()
|
||||
|
||||
checksumCalled := false
|
||||
restoreChecksum := swapUpdateFetchReleaseSHA256(func([]githubAsset) (map[string]string, error) {
|
||||
checksumCalled = true
|
||||
return nil, errors.New("checksum should not be fetched when dev build is already current")
|
||||
})
|
||||
defer restoreChecksum()
|
||||
|
||||
info, err := fetchLatestUpdateInfo(updateChannelDev)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
if checksumCalled {
|
||||
t.Fatal("expected dev channel checksum fetch to be skipped when build already matches")
|
||||
}
|
||||
if info.HasUpdate {
|
||||
t.Fatalf("expected HasUpdate=false, got %#v", info)
|
||||
}
|
||||
if info.Channel != string(updateChannelDev) || info.LatestVersion != "dev-a1b2c3d" {
|
||||
t.Fatalf("unexpected dev latest info: %#v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUpdateChannelPersistsAndClearsCachedUpdateState(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.configDir = t.TempDir()
|
||||
app.updateState.lastCheck = &UpdateInfo{
|
||||
HasUpdate: true,
|
||||
Channel: string(updateChannelLatest),
|
||||
LatestVersion: "0.6.5",
|
||||
}
|
||||
app.updateState.staged = &stagedUpdate{
|
||||
Channel: updateChannelLatest,
|
||||
Version: "0.6.5",
|
||||
AssetName: "GoNavi-0.6.5-Windows-Amd64.exe",
|
||||
}
|
||||
|
||||
result := app.SetUpdateChannel("dev")
|
||||
if !result.Success {
|
||||
t.Fatalf("SetUpdateChannel returned failure: %#v", result)
|
||||
}
|
||||
|
||||
stored, err := app.loadStoredUpdateChannel()
|
||||
if err != nil {
|
||||
t.Fatalf("loadStoredUpdateChannel returned error: %v", err)
|
||||
}
|
||||
if stored != updateChannelDev {
|
||||
t.Fatalf("expected stored dev channel, got %q", stored)
|
||||
}
|
||||
if app.updateState.lastCheck != nil || app.updateState.staged != nil {
|
||||
t.Fatalf("expected update cache to be cleared after channel switch, got %#v %#v", app.updateState.lastCheck, app.updateState.staged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReusableStagedUpdateDoesNotReuseDifferentChannelPackage(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
assetPath := filepath.Join(tempDir, "GoNavi-0.6.5-Windows-Amd64.exe")
|
||||
if err := os.WriteFile(assetPath, []byte("12345678"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
|
||||
reused := resolveReusableStagedUpdate(
|
||||
UpdateInfo{
|
||||
Channel: string(updateChannelLatest),
|
||||
LatestVersion: "0.6.5",
|
||||
AssetName: filepath.Base(assetPath),
|
||||
AssetSize: 8,
|
||||
},
|
||||
&stagedUpdate{
|
||||
Channel: updateChannelDev,
|
||||
Version: "0.6.5",
|
||||
AssetName: filepath.Base(assetPath),
|
||||
FilePath: assetPath,
|
||||
},
|
||||
)
|
||||
if reused != nil {
|
||||
t.Fatalf("expected staged update from another channel to be ignored, got %#v", reused)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadUpdateUsesCurrentLanguageForBackendMessage(t *testing.T) {
|
||||
app := NewApp()
|
||||
app.SetLanguage("en-US")
|
||||
|
||||
148
internal/app/update_channel_state.go
Normal file
148
internal/app/update_channel_state.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
updateChannelFileName = "update_channel.json"
|
||||
updateDevReleaseTag = "dev-latest"
|
||||
)
|
||||
|
||||
type updateChannel string
|
||||
|
||||
const (
|
||||
updateChannelLatest updateChannel = "latest"
|
||||
updateChannelDev updateChannel = "dev"
|
||||
)
|
||||
|
||||
type updateChannelStateFile struct {
|
||||
Channel updateChannel `json:"channel"`
|
||||
}
|
||||
|
||||
func normalizeUpdateChannel(value string) (updateChannel, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", string(updateChannelLatest):
|
||||
return updateChannelLatest, nil
|
||||
case string(updateChannelDev):
|
||||
return updateChannelDev, nil
|
||||
default:
|
||||
return "", localizedUpdateError{
|
||||
key: "app.update.backend.error.channel_invalid",
|
||||
params: map[string]any{"channel": strings.TrimSpace(value)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultUpdateChannel() updateChannel {
|
||||
version := strings.ToLower(strings.TrimSpace(getCurrentVersion()))
|
||||
if strings.HasPrefix(version, "dev-") {
|
||||
return updateChannelDev
|
||||
}
|
||||
return updateChannelLatest
|
||||
}
|
||||
|
||||
func updateChannelMetadataPath(configDir string) string {
|
||||
return filepath.Join(configDir, updateChannelFileName)
|
||||
}
|
||||
|
||||
func (a *App) loadStoredUpdateChannel() (updateChannel, error) {
|
||||
if strings.TrimSpace(a.configDir) == "" {
|
||||
a.configDir = resolveAppConfigDir()
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(updateChannelMetadataPath(a.configDir))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var state updateChannelStateFile
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizeUpdateChannel(string(state.Channel))
|
||||
}
|
||||
|
||||
func (a *App) persistUpdateChannel(channel updateChannel) error {
|
||||
if strings.TrimSpace(a.configDir) == "" {
|
||||
a.configDir = resolveAppConfigDir()
|
||||
}
|
||||
if err := os.MkdirAll(a.configDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.MarshalIndent(updateChannelStateFile{Channel: channel}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(updateChannelMetadataPath(a.configDir), payload, 0o644)
|
||||
}
|
||||
|
||||
func (a *App) currentUpdateChannel() updateChannel {
|
||||
if a == nil {
|
||||
return defaultUpdateChannel()
|
||||
}
|
||||
channel, err := a.loadStoredUpdateChannel()
|
||||
if err == nil {
|
||||
return channel
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Error(err, "加载更新通道配置失败")
|
||||
}
|
||||
return defaultUpdateChannel()
|
||||
}
|
||||
|
||||
func (a *App) GetUpdateChannel() connection.QueryResult {
|
||||
channel := a.currentUpdateChannel()
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: "OK",
|
||||
Data: map[string]any{
|
||||
"channel": string(channel),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SetUpdateChannel(channel string) connection.QueryResult {
|
||||
normalized, err := normalizeUpdateChannel(channel)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: a.localizedUpdateError(err)}
|
||||
}
|
||||
|
||||
a.updateMu.Lock()
|
||||
if a.updateState.downloading {
|
||||
a.updateMu.Unlock()
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.update.backend.message.channel_change_blocked_downloading", nil),
|
||||
}
|
||||
}
|
||||
a.updateMu.Unlock()
|
||||
|
||||
if err := a.persistUpdateChannel(normalized); err != nil {
|
||||
logger.Error(err, "保存更新通道失败")
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.update.backend.message.channel_change_failed", map[string]any{"detail": err.Error()}),
|
||||
}
|
||||
}
|
||||
|
||||
a.updateMu.Lock()
|
||||
a.updateState.lastCheck = nil
|
||||
a.updateState.staged = nil
|
||||
a.updateMu.Unlock()
|
||||
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: a.appText("app.update.backend.message.channel_changed", map[string]any{"channel": string(normalized)}),
|
||||
Data: map[string]any{
|
||||
"channel": string(normalized),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2426,9 +2426,11 @@
|
||||
"app.about.download_progress.title_with_version": "Update {{version}} herunterladen",
|
||||
"app.about.field.author": "Autor",
|
||||
"app.about.field.community": "Community",
|
||||
"app.about.field.update_channel": "Update-Kanal",
|
||||
"app.about.field.update_status": "Updatestatus",
|
||||
"app.about.field.version": "Version",
|
||||
"app.about.message.check_failed_with_error": "Updateprüfung fehlgeschlagen: {{error}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "Wechsel des Update-Kanals fehlgeschlagen: {{error}}",
|
||||
"app.about.message.download_completed": "Update-Download abgeschlossen",
|
||||
"app.about.message.download_completed_with_path": "Update-Download abgeschlossen. Paketpfad: {{path}}",
|
||||
"app.about.message.download_failed_with_error": "Update-Download fehlgeschlagen: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "Updatepaket ist bereit ({{version}}), Pfad: {{path}}",
|
||||
"app.about.project_links": "Projektlinks",
|
||||
"app.about.title": "Über GoNavi",
|
||||
"app.about.update_channel.dev": "Dev-Kanal",
|
||||
"app.about.update_channel.latest": "Latest-Kanal",
|
||||
"app.about.update_status.check_failed": "Updateprüfung fehlgeschlagen: {{error}}",
|
||||
"app.about.update_status.checking": "Suche nach Updates...",
|
||||
"app.about.update_status.latest": "Sie verwenden die neueste Version ({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "Installationsverzeichnis öffnen",
|
||||
"app.update.backend.error.check_failed": "Updateprüfung fehlgeschlagen: {{detail}}",
|
||||
"app.update.backend.error.check_http_status": "Updateprüfung fehlgeschlagen: HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "Ungültiger Update-Kanal: {{channel}}",
|
||||
"app.update.backend.error.download_failed": "Updatedownload fehlgeschlagen: {{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "Updateinstallation wird auf dieser Plattform nicht unterstützt: {{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "Neueste Versionsnummer konnte nicht gelesen werden",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "Updatepaket nicht gefunden: {{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "Auf das App-Verzeichnis kann nicht zugegriffen werden: {{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "Das aktuelle App-Verzeichnis kann nicht bestimmt werden, daher kann das Update nicht heruntergeladen werden",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "Ein Updatepaket wird gerade heruntergeladen, daher kann der Update-Kanal momentan nicht gewechselt werden",
|
||||
"app.update.backend.message.channel_change_failed": "Speichern des Update-Kanals fehlgeschlagen: {{detail}}",
|
||||
"app.update.backend.message.channel_changed": "Update-Kanal gewechselt zu: {{channel}}",
|
||||
"app.update.backend.message.check_first": "Prüfen Sie zuerst auf Updates",
|
||||
"app.update.backend.message.checksum_failed": "Prüfsumme des Updatepakets ist fehlgeschlagen. Versuchen Sie es erneut.",
|
||||
"app.update.backend.message.checksum_missing": "Prüfsumme des Updatepakets fehlt (SHA256SUMS)",
|
||||
|
||||
@@ -2426,9 +2426,11 @@
|
||||
"app.about.download_progress.title_with_version": "Download Update {{version}}",
|
||||
"app.about.field.author": "Author",
|
||||
"app.about.field.community": "Community",
|
||||
"app.about.field.update_channel": "Update Channel",
|
||||
"app.about.field.update_status": "Update Status",
|
||||
"app.about.field.version": "Version",
|
||||
"app.about.message.check_failed_with_error": "Update check failed: {{error}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "Failed to switch update channel: {{error}}",
|
||||
"app.about.message.download_completed": "Update download completed",
|
||||
"app.about.message.download_completed_with_path": "Update download completed. Package path: {{path}}",
|
||||
"app.about.message.download_failed_with_error": "Update download failed: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "Update package is ready ({{version}}), path: {{path}}",
|
||||
"app.about.project_links": "Project Links",
|
||||
"app.about.title": "About GoNavi",
|
||||
"app.about.update_channel.dev": "Dev Channel",
|
||||
"app.about.update_channel.latest": "Latest Channel",
|
||||
"app.about.update_status.check_failed": "Update check failed: {{error}}",
|
||||
"app.about.update_status.checking": "Checking for updates...",
|
||||
"app.about.update_status.latest": "You are on the latest version ({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "Open Install Directory",
|
||||
"app.update.backend.error.check_failed": "Check for updates failed: {{detail}}",
|
||||
"app.update.backend.error.check_http_status": "Check for updates failed: HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "Invalid update channel: {{channel}}",
|
||||
"app.update.backend.error.download_failed": "Update download failed: {{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "Update installation is not supported on this platform: {{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "Unable to parse the latest version",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "Update package not found: {{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "Cannot access app directory: {{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "Cannot determine the current app directory, so the update cannot be downloaded",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "An update package is downloading, so the update channel cannot be changed right now",
|
||||
"app.update.backend.message.channel_change_failed": "Failed to save update channel: {{detail}}",
|
||||
"app.update.backend.message.channel_changed": "Update channel switched to: {{channel}}",
|
||||
"app.update.backend.message.check_first": "Check for updates first",
|
||||
"app.update.backend.message.checksum_failed": "Update package checksum failed. Try again.",
|
||||
"app.update.backend.message.checksum_missing": "Update package checksum is missing (SHA256SUMS)",
|
||||
|
||||
@@ -2426,9 +2426,11 @@
|
||||
"app.about.download_progress.title_with_version": "更新をダウンロード {{version}}",
|
||||
"app.about.field.author": "作者",
|
||||
"app.about.field.community": "コミュニティ",
|
||||
"app.about.field.update_channel": "更新チャネル",
|
||||
"app.about.field.update_status": "更新状況",
|
||||
"app.about.field.version": "バージョン",
|
||||
"app.about.message.check_failed_with_error": "更新確認に失敗しました: {{error}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "更新チャネルの切り替えに失敗しました: {{error}}",
|
||||
"app.about.message.download_completed": "更新のダウンロードが完了しました",
|
||||
"app.about.message.download_completed_with_path": "更新のダウンロードが完了しました。パッケージパス: {{path}}",
|
||||
"app.about.message.download_failed_with_error": "更新のダウンロードに失敗しました: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "更新パッケージの準備ができました({{version}})。パス: {{path}}",
|
||||
"app.about.project_links": "プロジェクトリンク",
|
||||
"app.about.title": "GoNavi について",
|
||||
"app.about.update_channel.dev": "Dev チャネル",
|
||||
"app.about.update_channel.latest": "Latest チャネル",
|
||||
"app.about.update_status.check_failed": "更新確認に失敗しました: {{error}}",
|
||||
"app.about.update_status.checking": "更新を確認しています...",
|
||||
"app.about.update_status.latest": "現在は最新バージョンです({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "インストールディレクトリを開く",
|
||||
"app.update.backend.error.check_failed": "更新確認に失敗しました: {{detail}}",
|
||||
"app.update.backend.error.check_http_status": "更新確認に失敗しました: HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "無効な更新チャネルです: {{channel}}",
|
||||
"app.update.backend.error.download_failed": "更新のダウンロードに失敗しました: {{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "このプラットフォームでは更新のインストールに対応していません: {{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "最新バージョン番号を解析できません",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "更新パッケージが見つかりません: {{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "アプリディレクトリにアクセスできません: {{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "現在のアプリディレクトリを特定できないため、更新をダウンロードできません",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "更新パッケージをダウンロード中のため、今は更新チャネルを切り替えられません",
|
||||
"app.update.backend.message.channel_change_failed": "更新チャネルの保存に失敗しました: {{detail}}",
|
||||
"app.update.backend.message.channel_changed": "更新チャネルを切り替えました: {{channel}}",
|
||||
"app.update.backend.message.check_first": "先に更新を確認してください",
|
||||
"app.update.backend.message.checksum_failed": "更新パッケージのチェックサム検証に失敗しました。もう一度お試しください。",
|
||||
"app.update.backend.message.checksum_missing": "更新パッケージのチェックサムがありません (SHA256SUMS)",
|
||||
|
||||
@@ -2426,9 +2426,11 @@
|
||||
"app.about.download_progress.title_with_version": "Скачать обновление {{version}}",
|
||||
"app.about.field.author": "Автор",
|
||||
"app.about.field.community": "Сообщество",
|
||||
"app.about.field.update_channel": "Канал обновления",
|
||||
"app.about.field.update_status": "Статус обновления",
|
||||
"app.about.field.version": "Версия",
|
||||
"app.about.message.check_failed_with_error": "Проверка обновлений не удалась: {{error}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "Не удалось переключить канал обновления: {{error}}",
|
||||
"app.about.message.download_completed": "Загрузка обновления завершена",
|
||||
"app.about.message.download_completed_with_path": "Загрузка обновления завершена. Путь к пакету: {{path}}",
|
||||
"app.about.message.download_failed_with_error": "Не удалось скачать обновление: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "Пакет обновления готов ({{version}}), путь: {{path}}",
|
||||
"app.about.project_links": "Ссылки проекта",
|
||||
"app.about.title": "О GoNavi",
|
||||
"app.about.update_channel.dev": "Dev канал",
|
||||
"app.about.update_channel.latest": "Latest канал",
|
||||
"app.about.update_status.check_failed": "Проверка обновлений не удалась: {{error}}",
|
||||
"app.about.update_status.checking": "Проверка обновлений...",
|
||||
"app.about.update_status.latest": "У вас установлена последняя версия ({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "Открыть каталог установки",
|
||||
"app.update.backend.error.check_failed": "Не удалось проверить обновления: {{detail}}",
|
||||
"app.update.backend.error.check_http_status": "Не удалось проверить обновления: HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "Недопустимый канал обновления: {{channel}}",
|
||||
"app.update.backend.error.download_failed": "Не удалось скачать обновление: {{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "Установка обновления не поддерживается на этой платформе: {{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "Не удалось разобрать номер последней версии",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "Пакет обновления не найден: {{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "Нет доступа к каталогу приложения: {{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "Не удалось определить текущий каталог приложения, поэтому обновление нельзя скачать",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "Пакет обновления сейчас скачивается, поэтому канал обновления пока нельзя переключить",
|
||||
"app.update.backend.message.channel_change_failed": "Не удалось сохранить канал обновления: {{detail}}",
|
||||
"app.update.backend.message.channel_changed": "Канал обновления переключен на: {{channel}}",
|
||||
"app.update.backend.message.check_first": "Сначала проверьте обновления",
|
||||
"app.update.backend.message.checksum_failed": "Проверка контрольной суммы пакета обновления не пройдена. Повторите попытку.",
|
||||
"app.update.backend.message.checksum_missing": "Отсутствует контрольная сумма пакета обновления (SHA256SUMS)",
|
||||
|
||||
@@ -2426,11 +2426,13 @@
|
||||
"app.about.download_progress.title_with_version": "下载更新 {{version}}",
|
||||
"app.about.field.author": "作者",
|
||||
"app.about.field.community": "技术圈",
|
||||
"app.about.field.update_channel": "更新通道",
|
||||
"app.about.field.update_status": "更新状态",
|
||||
"app.about.field.version": "版本",
|
||||
"app.about.message.check_failed_with_error": "检查更新失败: {{error}}",
|
||||
"app.about.message.download_completed": "更新下载完成",
|
||||
"app.about.message.download_completed_with_path": "更新下载完成,更新包路径:{{path}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "切换更新通道失败:{{error}}",
|
||||
"app.about.message.download_failed_with_error": "更新下载失败: {{error}}",
|
||||
"app.about.message.install_directory_opened_manual_replace": "已打开安装目录,请手动完成替换",
|
||||
"app.about.message.install_failed_with_error": "更新安装失败: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "更新包已就绪({{version}}),路径:{{path}}",
|
||||
"app.about.project_links": "项目入口",
|
||||
"app.about.title": "关于 GoNavi",
|
||||
"app.about.update_channel.dev": "dev 通道",
|
||||
"app.about.update_channel.latest": "latest 通道",
|
||||
"app.about.update_status.check_failed": "检查更新失败: {{error}}",
|
||||
"app.about.update_status.checking": "正在检查更新...",
|
||||
"app.about.update_status.latest": "当前已是最新版本({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "打开安装目录",
|
||||
"app.update.backend.error.check_failed": "检查更新失败:{{detail}}",
|
||||
"app.update.backend.error.check_http_status": "检查更新失败:HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "无效的更新通道:{{channel}}",
|
||||
"app.update.backend.error.download_failed": "更新下载失败:{{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "当前平台暂不支持更新安装:{{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "无法解析最新版本号",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "未找到更新包:{{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "无法访问应用目录:{{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "无法确定当前应用目录,无法下载更新",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "更新包正在下载中,暂时不能切换更新通道",
|
||||
"app.update.backend.message.channel_change_failed": "保存更新通道失败:{{detail}}",
|
||||
"app.update.backend.message.channel_changed": "更新通道已切换为:{{channel}}",
|
||||
"app.update.backend.message.check_first": "请先检查更新",
|
||||
"app.update.backend.message.checksum_failed": "更新包校验失败,请重试",
|
||||
"app.update.backend.message.checksum_missing": "缺少更新包校验值(SHA256SUMS)",
|
||||
|
||||
@@ -2426,11 +2426,13 @@
|
||||
"app.about.download_progress.title_with_version": "下載更新 {{version}}",
|
||||
"app.about.field.author": "作者",
|
||||
"app.about.field.community": "技術圈",
|
||||
"app.about.field.update_channel": "更新通道",
|
||||
"app.about.field.update_status": "更新状态",
|
||||
"app.about.field.version": "版本",
|
||||
"app.about.message.check_failed_with_error": "檢查更新失敗: {{error}}",
|
||||
"app.about.message.download_completed": "更新下載完成",
|
||||
"app.about.message.download_completed_with_path": "更新下載完成,更新套件路徑:{{path}}",
|
||||
"app.about.message.channel_switch_failed_with_error": "切換更新通道失敗:{{error}}",
|
||||
"app.about.message.download_failed_with_error": "更新下載失敗: {{error}}",
|
||||
"app.about.message.install_directory_opened_manual_replace": "已開啟安裝目錄,請手動完成替換",
|
||||
"app.about.message.install_failed_with_error": "更新安裝失敗: {{error}}",
|
||||
@@ -2441,6 +2443,8 @@
|
||||
"app.about.message.update_package_ready_with_path": "更新套件已就緒({{version}}),路徑:{{path}}",
|
||||
"app.about.project_links": "專案入口",
|
||||
"app.about.title": "關於 GoNavi",
|
||||
"app.about.update_channel.dev": "dev 通道",
|
||||
"app.about.update_channel.latest": "latest 通道",
|
||||
"app.about.update_status.check_failed": "檢查更新失敗: {{error}}",
|
||||
"app.about.update_status.checking": "正在檢查更新...",
|
||||
"app.about.update_status.latest": "目前已是最新版本({{version}})",
|
||||
@@ -2862,6 +2866,7 @@
|
||||
"app.update.action.open_install_directory": "開啟安裝目錄",
|
||||
"app.update.backend.error.check_failed": "檢查更新失敗:{{detail}}",
|
||||
"app.update.backend.error.check_http_status": "檢查更新失敗:HTTP {{status}}",
|
||||
"app.update.backend.error.channel_invalid": "無效的更新通道:{{channel}}",
|
||||
"app.update.backend.error.download_failed": "更新下載失敗:{{detail}}",
|
||||
"app.update.backend.error.install_unsupported": "目前平台暫不支援更新安裝:{{platform}}",
|
||||
"app.update.backend.error.latest_version_unparseable": "無法解析最新版本號",
|
||||
@@ -2875,6 +2880,9 @@
|
||||
"app.update.backend.error.update_package_not_found": "未找到更新套件:{{name}}",
|
||||
"app.update.backend.message.app_directory_unavailable": "無法存取應用程式目錄:{{path}}",
|
||||
"app.update.backend.message.app_directory_unresolved_download": "無法判斷目前應用程式目錄,因此無法下載更新",
|
||||
"app.update.backend.message.channel_change_blocked_downloading": "更新套件正在下載中,暫時不能切換更新通道",
|
||||
"app.update.backend.message.channel_change_failed": "保存更新通道失敗:{{detail}}",
|
||||
"app.update.backend.message.channel_changed": "更新通道已切換為:{{channel}}",
|
||||
"app.update.backend.message.check_first": "請先檢查更新",
|
||||
"app.update.backend.message.checksum_failed": "更新套件校驗失敗,請重試",
|
||||
"app.update.backend.message.checksum_missing": "缺少更新套件校驗值(SHA256SUMS)",
|
||||
|
||||
Reference in New Issue
Block a user