mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
feat: add auto check update
This commit is contained in:
@@ -258,26 +258,57 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
}, [handleSyncNow]);
|
||||
|
||||
// Update check hook - checks for new versions on startup
|
||||
const { updateState, openReleasePage, dismissUpdate } = useUpdateCheck();
|
||||
const { updateState, openReleasePage, downloadUpdate, installUpdate } = useUpdateCheck();
|
||||
|
||||
const handleUpdateAction = useCallback(async () => {
|
||||
if (updateState.updateDownloaded) {
|
||||
const installed = await installUpdate();
|
||||
if (!installed) {
|
||||
openReleasePage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (updateState.isDownloading) {
|
||||
return;
|
||||
}
|
||||
const started = await downloadUpdate();
|
||||
if (!started) {
|
||||
openReleasePage();
|
||||
}
|
||||
}, [
|
||||
updateState.updateDownloaded,
|
||||
updateState.isDownloading,
|
||||
installUpdate,
|
||||
downloadUpdate,
|
||||
openReleasePage,
|
||||
]);
|
||||
|
||||
// Show toast notification when update is available
|
||||
useEffect(() => {
|
||||
if (updateState.hasUpdate && updateState.latestRelease) {
|
||||
const version = updateState.latestRelease.version;
|
||||
toast.info(
|
||||
t('update.available.message', { version }),
|
||||
{
|
||||
title: t('update.available.title'),
|
||||
duration: 8000, // Show longer for update notifications
|
||||
onClick: () => {
|
||||
openReleasePage();
|
||||
dismissUpdate();
|
||||
},
|
||||
actionLabel: t('update.downloadNow'),
|
||||
}
|
||||
);
|
||||
toast.info(t('update.available.message', { version }), {
|
||||
title: t('update.available.title'),
|
||||
duration: 8000, // Show longer for update notifications
|
||||
onClick: () => {
|
||||
void handleUpdateAction();
|
||||
},
|
||||
actionLabel: t('update.downloadNow'),
|
||||
});
|
||||
}
|
||||
}, [updateState.hasUpdate, updateState.latestRelease, t, openReleasePage, dismissUpdate]);
|
||||
}, [updateState.hasUpdate, updateState.latestRelease, t, handleUpdateAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!updateState.updateDownloaded) return;
|
||||
toast.success(t('update.downloaded.message'), {
|
||||
title: t('update.downloaded.title'),
|
||||
duration: 8000,
|
||||
onClick: () => {
|
||||
void handleUpdateAction();
|
||||
},
|
||||
actionLabel: t('update.installNow'),
|
||||
});
|
||||
}, [updateState.updateDownloaded, t, handleUpdateAction]);
|
||||
|
||||
// Debounce ref for moveFocus to prevent double-triggering when focus switches
|
||||
const lastMoveFocusTimeRef = useRef<number>(0);
|
||||
|
||||
@@ -76,10 +76,14 @@ const en: Messages = {
|
||||
'update.available.title': 'Update Available',
|
||||
'update.available.message': 'A new version {version} is available. Click to download.',
|
||||
'update.checking': 'Checking for updates...',
|
||||
'update.downloading': 'Downloading update... {percent}%',
|
||||
'update.downloaded.title': 'Update Ready',
|
||||
'update.downloaded.message': 'Update downloaded. Restart to install.',
|
||||
'update.upToDate.title': 'Up to Date',
|
||||
'update.upToDate.message': 'You are running the latest version ({version}).',
|
||||
'update.error': 'Failed to check for updates',
|
||||
'update.downloadNow': 'Download Now',
|
||||
'update.installNow': 'Install and Restart',
|
||||
'update.remindLater': 'Remind Later',
|
||||
'update.skipVersion': 'Skip This Version',
|
||||
|
||||
|
||||
@@ -64,10 +64,14 @@ const zhCN: Messages = {
|
||||
'update.available.title': '发现新版本',
|
||||
'update.available.message': '新版本 {version} 已发布,点击前往下载。',
|
||||
'update.checking': '正在检查更新...',
|
||||
'update.downloading': '正在下载更新... {percent}%',
|
||||
'update.downloaded.title': '更新已就绪',
|
||||
'update.downloaded.message': '更新已下载完成,重启即可安装。',
|
||||
'update.upToDate.title': '已是最新版本',
|
||||
'update.upToDate.message': '当前版本 ({version}) 已是最新。',
|
||||
'update.error': '检查更新失败',
|
||||
'update.downloadNow': '立即下载',
|
||||
'update.installNow': '安装并重启',
|
||||
'update.remindLater': '稍后提醒',
|
||||
'update.skipVersion': '跳过此版本',
|
||||
|
||||
|
||||
@@ -21,11 +21,15 @@ const debugLog = (...args: unknown[]) => {
|
||||
|
||||
export interface UpdateState {
|
||||
isChecking: boolean;
|
||||
isDownloading: boolean;
|
||||
updateDownloaded: boolean;
|
||||
downloadProgress: number | null;
|
||||
hasUpdate: boolean;
|
||||
currentVersion: string;
|
||||
latestRelease: ReleaseInfo | null;
|
||||
error: string | null;
|
||||
lastCheckedAt: number | null;
|
||||
autoUpdateSupported: boolean;
|
||||
}
|
||||
|
||||
export interface UseUpdateCheckResult {
|
||||
@@ -33,8 +37,28 @@ export interface UseUpdateCheckResult {
|
||||
checkNow: () => Promise<UpdateCheckResult | null>;
|
||||
dismissUpdate: () => void;
|
||||
openReleasePage: () => void;
|
||||
downloadUpdate: () => Promise<boolean>;
|
||||
installUpdate: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
type UpdateStatusPayload = {
|
||||
status?: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
supported?: boolean;
|
||||
updateInfo?: {
|
||||
version?: string;
|
||||
releaseName?: string;
|
||||
releaseNotes?: string;
|
||||
releaseDate?: string;
|
||||
} | null;
|
||||
progress?: {
|
||||
percent?: number;
|
||||
transferred?: number;
|
||||
total?: number;
|
||||
bytesPerSecond?: number;
|
||||
} | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for managing update checks
|
||||
* - Automatically checks for updates on startup (with delay)
|
||||
@@ -44,11 +68,15 @@ export interface UseUpdateCheckResult {
|
||||
export function useUpdateCheck(): UseUpdateCheckResult {
|
||||
const [updateState, setUpdateState] = useState<UpdateState>({
|
||||
isChecking: false,
|
||||
isDownloading: false,
|
||||
updateDownloaded: false,
|
||||
downloadProgress: null,
|
||||
hasUpdate: false,
|
||||
currentVersion: '',
|
||||
latestRelease: null,
|
||||
error: null,
|
||||
lastCheckedAt: null,
|
||||
autoUpdateSupported: false,
|
||||
});
|
||||
|
||||
const hasCheckedOnStartupRef = useRef(false);
|
||||
@@ -71,6 +99,85 @@ export function useUpdateCheck(): UseUpdateCheckResult {
|
||||
void loadVersion();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUpdateStatus = async () => {
|
||||
try {
|
||||
const bridge = netcattyBridge.get();
|
||||
const status = await bridge?.getUpdateStatus?.();
|
||||
if (!status) return;
|
||||
setUpdateState((prev) => ({
|
||||
...prev,
|
||||
autoUpdateSupported: Boolean(status.supported),
|
||||
isDownloading: status.status === 'downloading',
|
||||
updateDownloaded: status.status === 'downloaded',
|
||||
downloadProgress: status.progress?.percent ?? prev.downloadProgress,
|
||||
error: status.error ?? prev.error,
|
||||
}));
|
||||
} catch {
|
||||
// Ignore if auto updater isn't available
|
||||
}
|
||||
};
|
||||
void loadUpdateStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onUpdateStatus) return;
|
||||
const unsubscribe = bridge.onUpdateStatus((payload: UpdateStatusPayload) => {
|
||||
if (!payload) return;
|
||||
setUpdateState((prev) => {
|
||||
const next = { ...prev };
|
||||
if (typeof payload.supported === 'boolean') {
|
||||
next.autoUpdateSupported = payload.supported;
|
||||
}
|
||||
|
||||
if (payload.status === 'checking') {
|
||||
next.isChecking = true;
|
||||
next.error = null;
|
||||
} else if (payload.status === 'available') {
|
||||
next.isChecking = false;
|
||||
next.hasUpdate = true;
|
||||
if (!next.latestRelease && payload.updateInfo?.version) {
|
||||
next.latestRelease = {
|
||||
version: payload.updateInfo.version,
|
||||
tagName: `v${payload.updateInfo.version}`,
|
||||
name: payload.updateInfo.releaseName || payload.updateInfo.version,
|
||||
body: payload.updateInfo.releaseNotes || '',
|
||||
htmlUrl: getReleaseUrl(payload.updateInfo.version),
|
||||
publishedAt: payload.updateInfo.releaseDate || '',
|
||||
assets: [],
|
||||
};
|
||||
}
|
||||
} else if (payload.status === 'downloading') {
|
||||
next.isDownloading = true;
|
||||
next.updateDownloaded = false;
|
||||
next.hasUpdate = true;
|
||||
next.downloadProgress = payload.progress?.percent ?? next.downloadProgress;
|
||||
next.error = null;
|
||||
} else if (payload.status === 'downloaded') {
|
||||
next.isDownloading = false;
|
||||
next.updateDownloaded = true;
|
||||
next.hasUpdate = true;
|
||||
next.downloadProgress = 100;
|
||||
next.error = null;
|
||||
} else if (payload.status === 'error') {
|
||||
next.isChecking = false;
|
||||
next.isDownloading = false;
|
||||
next.error = payload.error || 'Unknown error';
|
||||
} else if (payload.status === 'not-available') {
|
||||
next.isChecking = false;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const performCheck = useCallback(async (currentVersion: string): Promise<UpdateCheckResult | null> => {
|
||||
debugLog('performCheck called', { currentVersion, IS_UPDATE_DEMO_MODE });
|
||||
|
||||
@@ -92,6 +199,18 @@ export function useUpdateCheck(): UseUpdateCheckResult {
|
||||
setUpdateState((prev) => ({ ...prev, isChecking: true, error: null }));
|
||||
|
||||
try {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.updateCheck) {
|
||||
void bridge.updateCheck().then((status) => {
|
||||
if (status && typeof status.supported === 'boolean') {
|
||||
setUpdateState((prev) => ({ ...prev, autoUpdateSupported: status.supported }));
|
||||
}
|
||||
if (status?.error) {
|
||||
setUpdateState((prev) => ({ ...prev, error: status.error || 'Unknown error' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let result: UpdateCheckResult;
|
||||
|
||||
if (IS_UPDATE_DEMO_MODE) {
|
||||
@@ -189,6 +308,55 @@ export function useUpdateCheck(): UseUpdateCheckResult {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}, [updateState.latestRelease]);
|
||||
|
||||
const downloadUpdate = useCallback(async () => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.downloadUpdate) return false;
|
||||
setUpdateState((prev) => ({ ...prev, isDownloading: true, error: null }));
|
||||
try {
|
||||
const result = await bridge.downloadUpdate();
|
||||
if (result && typeof result.supported === 'boolean') {
|
||||
setUpdateState((prev) => ({ ...prev, autoUpdateSupported: result.supported }));
|
||||
}
|
||||
if (result?.error) {
|
||||
setUpdateState((prev) => ({
|
||||
...prev,
|
||||
isDownloading: false,
|
||||
error: result.error || 'Unknown error',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
if (result?.supported === false) {
|
||||
setUpdateState((prev) => ({ ...prev, isDownloading: false }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
setUpdateState((prev) => ({ ...prev, isDownloading: false, error: errorMsg }));
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const installUpdate = useCallback(async () => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.installUpdate) return false;
|
||||
try {
|
||||
const result = await bridge.installUpdate();
|
||||
if (result?.error) {
|
||||
setUpdateState((prev) => ({ ...prev, error: result.error || 'Unknown error' }));
|
||||
return false;
|
||||
}
|
||||
if (result?.supported === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
setUpdateState((prev) => ({ ...prev, error: errorMsg }));
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Startup check with delay - runs once on mount
|
||||
useEffect(() => {
|
||||
debugLog('Startup check effect mounted, IS_UPDATE_DEMO_MODE:', IS_UPDATE_DEMO_MODE);
|
||||
@@ -261,5 +429,7 @@ export function useUpdateCheck(): UseUpdateCheckResult {
|
||||
checkNow,
|
||||
dismissUpdate,
|
||||
openReleasePage,
|
||||
downloadUpdate,
|
||||
installUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ const ActionRow: React.FC<{
|
||||
export default function SettingsApplicationTab() {
|
||||
const { t } = useI18n();
|
||||
const { openExternal, getApplicationInfo } = useApplicationBackend();
|
||||
const { updateState, checkNow, openReleasePage } = useUpdateCheck();
|
||||
const { updateState, checkNow, openReleasePage, downloadUpdate, installUpdate } = useUpdateCheck();
|
||||
const [appInfo, setAppInfo] = useState<AppInfo>({ name: "Netcatty", version: "" });
|
||||
const [lastCheckResult, setLastCheckResult] = useState<'none' | 'available' | 'upToDate'>('none');
|
||||
const [hasAutoChecked, setHasAutoChecked] = useState(false);
|
||||
@@ -106,6 +106,25 @@ export default function SettingsApplicationTab() {
|
||||
void checkNow();
|
||||
}, [hasAutoChecked, updateState.isChecking, isUpdateDemoMode, appInfo.version, checkNow]);
|
||||
|
||||
const handleUpdateAction = async () => {
|
||||
if (updateState.updateDownloaded) {
|
||||
const installed = await installUpdate();
|
||||
if (!installed) {
|
||||
openReleasePage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateState.isDownloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const started = await downloadUpdate();
|
||||
if (!started) {
|
||||
openReleasePage();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckForUpdates = async () => {
|
||||
// In demo mode, allow checking even for dev builds
|
||||
if (!isUpdateDemoMode && (!appInfo.version || appInfo.version === '0.0.0')) {
|
||||
@@ -120,12 +139,13 @@ export default function SettingsApplicationTab() {
|
||||
|
||||
if (result?.hasUpdate && result.latestRelease) {
|
||||
setLastCheckResult('available');
|
||||
toast.info(
|
||||
t('update.available.message', { version: result.latestRelease.version }),
|
||||
t('update.available.title')
|
||||
);
|
||||
// Open the release page
|
||||
openReleasePage();
|
||||
toast.info(t('update.available.message', { version: result.latestRelease.version }), {
|
||||
title: t('update.available.title'),
|
||||
onClick: () => {
|
||||
void handleUpdateAction();
|
||||
},
|
||||
actionLabel: t('update.downloadNow'),
|
||||
});
|
||||
} else if (result) {
|
||||
setLastCheckResult('upToDate');
|
||||
toast.success(
|
||||
@@ -141,6 +161,15 @@ export default function SettingsApplicationTab() {
|
||||
const issueUrl = useMemo(() => buildIssueUrl(appInfo), [appInfo]);
|
||||
const releasesUrl = `${REPO_URL}/releases`;
|
||||
const discussionsUrl = `${REPO_URL}/discussions`;
|
||||
const downloadPercent = updateState.downloadProgress != null
|
||||
? Math.round(updateState.downloadProgress)
|
||||
: null;
|
||||
|
||||
const updateActionLabel = updateState.updateDownloaded
|
||||
? t('update.installNow')
|
||||
: updateState.isDownloading
|
||||
? t('update.downloading', { percent: downloadPercent ?? 0 })
|
||||
: t('update.downloadNow');
|
||||
|
||||
return (
|
||||
<SettingsTabContent value="application">
|
||||
@@ -157,15 +186,16 @@ export default function SettingsApplicationTab() {
|
||||
{/* Update available badge - inline with version */}
|
||||
{updateState.hasUpdate && updateState.latestRelease && (
|
||||
<button
|
||||
onClick={() => void openReleasePage()}
|
||||
onClick={() => void handleUpdateAction()}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
|
||||
"bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300",
|
||||
"hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer"
|
||||
"hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer",
|
||||
updateState.isDownloading && "opacity-70 cursor-default"
|
||||
)}
|
||||
>
|
||||
<ArrowUpCircle size={12} />
|
||||
v{updateState.latestRelease.version} {t('update.downloadNow')}
|
||||
v{updateState.latestRelease.version} {updateActionLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -79,5 +79,12 @@
|
||||
}
|
||||
],
|
||||
"category": "Development"
|
||||
}
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "binaricat",
|
||||
"repo": "Netcatty"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Update Bridge - Handles auto updates via electron-updater
|
||||
*/
|
||||
|
||||
let electronModule = null;
|
||||
let autoUpdater;
|
||||
let autoUpdaterLoaded = false;
|
||||
let listenersAttached = false;
|
||||
|
||||
const updateStatus = {
|
||||
status: "idle",
|
||||
supported: false,
|
||||
updateInfo: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function loadAutoUpdater() {
|
||||
if (autoUpdaterLoaded) return autoUpdater;
|
||||
autoUpdaterLoaded = true;
|
||||
try {
|
||||
autoUpdater = require("electron-updater").autoUpdater;
|
||||
} catch (err) {
|
||||
console.warn("[UpdateBridge] electron-updater not available:", err?.message || err);
|
||||
autoUpdater = null;
|
||||
}
|
||||
return autoUpdater;
|
||||
}
|
||||
|
||||
function isSupported() {
|
||||
return !!loadAutoUpdater() && !!electronModule?.app?.isPackaged;
|
||||
}
|
||||
|
||||
function normalizeReleaseNotes(notes) {
|
||||
if (!notes) return "";
|
||||
if (typeof notes === "string") return notes;
|
||||
if (Array.isArray(notes)) {
|
||||
return notes
|
||||
.map((entry) => entry?.note || "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function mapUpdateInfo(info) {
|
||||
if (!info) return null;
|
||||
return {
|
||||
version: info.version || "",
|
||||
releaseName: info.releaseName || info.version || "",
|
||||
releaseNotes: normalizeReleaseNotes(info.releaseNotes),
|
||||
releaseDate: info.releaseDate || "",
|
||||
};
|
||||
}
|
||||
|
||||
function mapProgress(progress) {
|
||||
if (!progress) return null;
|
||||
return {
|
||||
percent: typeof progress.percent === "number" ? progress.percent : 0,
|
||||
transferred: typeof progress.transferred === "number" ? progress.transferred : 0,
|
||||
total: typeof progress.total === "number" ? progress.total : 0,
|
||||
bytesPerSecond: typeof progress.bytesPerSecond === "number" ? progress.bytesPerSecond : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastStatus() {
|
||||
const { BrowserWindow } = electronModule || {};
|
||||
if (!BrowserWindow?.getAllWindows) return;
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach((win) => {
|
||||
if (!win || win.isDestroyed()) return;
|
||||
win.webContents?.send?.("netcatty:update:status", updateStatus);
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(next) {
|
||||
updateStatus.status = next.status ?? updateStatus.status;
|
||||
updateStatus.updateInfo = next.updateInfo ?? updateStatus.updateInfo;
|
||||
updateStatus.progress = next.progress ?? updateStatus.progress;
|
||||
updateStatus.error = next.error ?? updateStatus.error;
|
||||
updateStatus.supported = isSupported();
|
||||
broadcastStatus();
|
||||
}
|
||||
|
||||
function attachListeners() {
|
||||
if (!isSupported() || listenersAttached) return;
|
||||
const updater = loadAutoUpdater();
|
||||
if (!updater) return;
|
||||
listenersAttached = true;
|
||||
|
||||
updater.on("checking-for-update", () => {
|
||||
setStatus({ status: "checking", error: null });
|
||||
});
|
||||
|
||||
updater.on("update-available", (info) => {
|
||||
setStatus({ status: "available", updateInfo: mapUpdateInfo(info), error: null });
|
||||
});
|
||||
|
||||
updater.on("update-not-available", (info) => {
|
||||
setStatus({ status: "not-available", updateInfo: mapUpdateInfo(info), error: null });
|
||||
});
|
||||
|
||||
updater.on("download-progress", (progress) => {
|
||||
setStatus({ status: "downloading", progress: mapProgress(progress), error: null });
|
||||
});
|
||||
|
||||
updater.on("update-downloaded", (info) => {
|
||||
setStatus({ status: "downloaded", updateInfo: mapUpdateInfo(info), progress: null, error: null });
|
||||
});
|
||||
|
||||
updater.on("error", (err) => {
|
||||
setStatus({
|
||||
status: "error",
|
||||
error: err?.message || String(err),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init(deps) {
|
||||
electronModule = deps?.electronModule || null;
|
||||
updateStatus.supported = isSupported();
|
||||
}
|
||||
|
||||
function registerHandlers(ipcMain) {
|
||||
if (!ipcMain) return;
|
||||
const updater = loadAutoUpdater();
|
||||
if (updater) {
|
||||
updater.autoDownload = false;
|
||||
}
|
||||
attachListeners();
|
||||
|
||||
ipcMain.handle("netcatty:update:getStatus", async () => {
|
||||
updateStatus.supported = isSupported();
|
||||
return { ...updateStatus };
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:update:check", async () => {
|
||||
if (!isSupported()) {
|
||||
return { supported: false, error: "Auto updates are unavailable in this build." };
|
||||
}
|
||||
attachListeners();
|
||||
try {
|
||||
setStatus({ status: "checking", error: null });
|
||||
const result = await updater.checkForUpdates();
|
||||
const info = mapUpdateInfo(result?.updateInfo);
|
||||
if (info) {
|
||||
updateStatus.updateInfo = info;
|
||||
}
|
||||
return { supported: true, updateInfo: info };
|
||||
} catch (err) {
|
||||
const message = err?.message || String(err);
|
||||
setStatus({ status: "error", error: message });
|
||||
return { supported: true, error: message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:update:download", async () => {
|
||||
if (!isSupported()) {
|
||||
return { supported: false, error: "Auto updates are unavailable in this build." };
|
||||
}
|
||||
attachListeners();
|
||||
try {
|
||||
if (!updateStatus.updateInfo) {
|
||||
await updater.checkForUpdates();
|
||||
}
|
||||
await updater.downloadUpdate();
|
||||
return { supported: true };
|
||||
} catch (err) {
|
||||
const message = err?.message || String(err);
|
||||
setStatus({ status: "error", error: message });
|
||||
return { supported: true, error: message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("netcatty:update:install", async () => {
|
||||
if (!isSupported()) {
|
||||
return { supported: false, error: "Auto updates are unavailable in this build." };
|
||||
}
|
||||
try {
|
||||
updater.quitAndInstall();
|
||||
return { supported: true };
|
||||
} catch (err) {
|
||||
const message = err?.message || String(err);
|
||||
setStatus({ status: "error", error: message });
|
||||
return { supported: true, error: message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
registerHandlers,
|
||||
};
|
||||
@@ -77,6 +77,7 @@ const googleAuthBridge = require("./bridges/googleAuthBridge.cjs");
|
||||
const onedriveAuthBridge = require("./bridges/onedriveAuthBridge.cjs");
|
||||
const cloudSyncBridge = require("./bridges/cloudSyncBridge.cjs");
|
||||
const windowManager = require("./bridges/windowManager.cjs");
|
||||
const updateBridge = require("./bridges/updateBridge.cjs");
|
||||
|
||||
// GPU settings
|
||||
// NOTE: Do not disable Chromium sandbox by default.
|
||||
@@ -359,6 +360,7 @@ const registerBridges = (win) => {
|
||||
sftpBridge.init(deps);
|
||||
transferBridge.init(deps);
|
||||
terminalBridge.init(deps);
|
||||
updateBridge.init({ electronModule });
|
||||
|
||||
// Register all IPC handlers
|
||||
sshBridge.registerHandlers(ipcMain);
|
||||
@@ -372,6 +374,7 @@ const registerBridges = (win) => {
|
||||
googleAuthBridge.registerHandlers(ipcMain, electronModule);
|
||||
onedriveAuthBridge.registerHandlers(ipcMain, electronModule);
|
||||
cloudSyncBridge.registerHandlers(ipcMain);
|
||||
updateBridge.registerHandlers(ipcMain);
|
||||
|
||||
// Settings window handler
|
||||
ipcMain.handle("netcatty:settings:open", async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ const chainProgressListeners = new Map();
|
||||
const authFailedListeners = new Map();
|
||||
const languageChangeListeners = new Set();
|
||||
const fullscreenChangeListeners = new Set();
|
||||
const updateStatusListeners = new Set();
|
||||
|
||||
ipcRenderer.on("netcatty:data", (_event, payload) => {
|
||||
const set = dataListeners.get(payload.sessionId);
|
||||
@@ -70,6 +71,16 @@ ipcRenderer.on("netcatty:window:fullscreen-changed", (_event, isFullscreen) => {
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on("netcatty:update:status", (_event, payload) => {
|
||||
updateStatusListeners.forEach((cb) => {
|
||||
try {
|
||||
cb(payload);
|
||||
} catch (err) {
|
||||
console.error("Update status callback failed", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Authentication failed events
|
||||
@@ -362,6 +373,10 @@ const api = {
|
||||
fullscreenChangeListeners.add(cb);
|
||||
return () => fullscreenChangeListeners.delete(cb);
|
||||
},
|
||||
onUpdateStatus: (cb) => {
|
||||
updateStatusListeners.add(cb);
|
||||
return () => updateStatusListeners.delete(cb);
|
||||
},
|
||||
|
||||
// Settings window
|
||||
openSettingsWindow: () => ipcRenderer.invoke("netcatty:settings:open"),
|
||||
@@ -407,6 +422,10 @@ const api = {
|
||||
|
||||
// App info
|
||||
getAppInfo: () => ipcRenderer.invoke("netcatty:app:getInfo"),
|
||||
getUpdateStatus: () => ipcRenderer.invoke("netcatty:update:getStatus"),
|
||||
updateCheck: () => ipcRenderer.invoke("netcatty:update:check"),
|
||||
downloadUpdate: () => ipcRenderer.invoke("netcatty:update:download"),
|
||||
installUpdate: () => ipcRenderer.invoke("netcatty:update:install"),
|
||||
|
||||
// Tell main process the renderer has mounted/painted (used to avoid initial blank screen).
|
||||
rendererReady: () => ipcRenderer.send("netcatty:renderer:ready"),
|
||||
|
||||
Vendored
+35
@@ -111,6 +111,34 @@ interface PortForwardStatusResult {
|
||||
|
||||
type PortForwardStatusCallback = (status: 'inactive' | 'connecting' | 'active' | 'error', error?: string) => void;
|
||||
|
||||
interface NetcattyUpdateInfo {
|
||||
version: string;
|
||||
releaseName?: string;
|
||||
releaseNotes?: string;
|
||||
releaseDate?: string;
|
||||
}
|
||||
|
||||
interface NetcattyUpdateProgress {
|
||||
percent?: number;
|
||||
transferred?: number;
|
||||
total?: number;
|
||||
bytesPerSecond?: number;
|
||||
}
|
||||
|
||||
interface NetcattyUpdateStatus {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
supported?: boolean;
|
||||
updateInfo?: NetcattyUpdateInfo | null;
|
||||
progress?: NetcattyUpdateProgress | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
interface NetcattyUpdateActionResult {
|
||||
supported: boolean;
|
||||
updateInfo?: NetcattyUpdateInfo | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface NetcattyBridge {
|
||||
startSSHSession(options: NetcattySSHOptions): Promise<string>;
|
||||
startTelnetSession?(options: {
|
||||
@@ -231,6 +259,13 @@ interface NetcattyBridge {
|
||||
windowIsMaximized?(): Promise<boolean>;
|
||||
windowIsFullscreen?(): Promise<boolean>;
|
||||
onWindowFullScreenChanged?(cb: (isFullscreen: boolean) => void): () => void;
|
||||
|
||||
// Auto update
|
||||
getUpdateStatus?(): Promise<NetcattyUpdateStatus>;
|
||||
updateCheck?(): Promise<NetcattyUpdateActionResult>;
|
||||
downloadUpdate?(): Promise<NetcattyUpdateActionResult>;
|
||||
installUpdate?(): Promise<NetcattyUpdateActionResult>;
|
||||
onUpdateStatus?(cb: (status: NetcattyUpdateStatus) => void): () => void;
|
||||
|
||||
// Settings window
|
||||
openSettingsWindow?(): Promise<boolean>;
|
||||
|
||||
Generated
+84
-6
@@ -29,6 +29,7 @@
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"clsx": "2.1.1",
|
||||
"electron-updater": "^6.6.4",
|
||||
"lucide-react": "0.560.0",
|
||||
"node-pty": "1.1.0-beta19",
|
||||
"react": "^19.2.1",
|
||||
@@ -6278,7 +6279,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
@@ -7706,6 +7706,70 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-updater": {
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz",
|
||||
"integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"builder-util-runtime": "9.5.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"semver": "~7.7.3",
|
||||
"tiny-typed-emitter": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/builder-util-runtime": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
|
||||
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-winstaller": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||
@@ -8791,7 +8855,6 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
@@ -9302,7 +9365,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -9426,7 +9488,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/levn": {
|
||||
@@ -9727,6 +9788,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -10941,7 +11015,6 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
@@ -10954,7 +11027,6 @@
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -11612,6 +11684,12 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
Reference in New Issue
Block a user