Merge pull request #523 from hydralauncher/feat/refactor-auto-update

feat/refactor auto update
This commit is contained in:
Zamitto 2024-05-27 15:43:44 -03:00 committed by GitHub
commit f9996d0646
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 132 additions and 313 deletions

View file

@ -29,7 +29,8 @@
"catalogue": "Catalogue",
"downloads": "Downloads",
"search_results": "Search results",
"settings": "Settings"
"settings": "Settings",
"version_available": "Version {{version}} available. Click here to restart and install."
},
"bottom_panel": {
"no_downloads_in_progress": "No downloads in progress",
@ -176,11 +177,5 @@
},
"modal": {
"close": "Close button"
},
"splash": {
"downloading_version": "Downloading version {{version}}",
"searching_updates": "Searching for updates",
"update_found": "Update {{version}} found",
"restarting_and_applying": "Restarting and applying update"
}
}

View file

@ -176,11 +176,5 @@
},
"modal": {
"close": "Botón de cierre"
},
"splash": {
"downloading_version": "Descargando versión {{version}}",
"searching_updates": "Buscando actualizaciones",
"update_found": "Actualización {{version}} encontrada",
"restarting_and_applying": "Reiniciando y aplicando actualización"
}
}

View file

@ -176,11 +176,5 @@
},
"modal": {
"close": "Pulsante Chiudi"
},
"splash": {
"downloading_version": "Scaricando la versione {{version}}",
"searching_updates": "Ricerca di aggiornamenti",
"update_found": "Trovato aggiornamento {{version}}",
"restarting_and_applying": "Riavvio e applico l'aggiornamento"
}
}

View file

@ -29,7 +29,8 @@
"downloads": "Downloads",
"search_results": "Resultados da busca",
"settings": "Configurações",
"home": "Início"
"home": "Início",
"version_available": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar."
},
"bottom_panel": {
"no_downloads_in_progress": "Sem downloads em andamento",

View file

@ -176,11 +176,5 @@
},
"modal": {
"close": "Закрыть"
},
"splash": {
"downloading_version": "Загрузка версии {{version}}",
"searching_updates": "Поиск обновлений",
"update_found": "Найдена новая версия {{version}}",
"restarting_and_applying": "Перезапуск и внесение изменений"
}
}

View file

@ -174,11 +174,5 @@
},
"modal": {
"close": "关闭按钮"
},
"splash": {
"downloading_version": "正在下载新版本 {{version}}",
"searching_updates": "检查更新...",
"update_found": "有新版本 {{version}} 可用",
"restarting_and_applying": "重启并应用更新"
}
}

View file

@ -1,41 +1,27 @@
import { AppUpdaterEvents } from "@types";
import { registerEvent } from "../register-event";
import updater, { ProgressInfo, UpdateInfo } from "electron-updater";
import updater, { UpdateInfo } from "electron-updater";
import { WindowManager } from "@main/services";
import { app } from "electron";
const { autoUpdater } = updater;
const sendEvent = (event: AppUpdaterEvents) => {
WindowManager.splashWindow?.webContents.send("autoUpdaterEvent", event);
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
};
const mockValuesForDebug = async () => {
sendEvent({ type: "update-downloaded" });
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
// sendEvent({ type: "update-downloaded" });
};
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater
.addListener("error", () => {
sendEvent({ type: "error" });
})
.addListener("checking-for-update", () => {
sendEvent({ type: "checking-for-updates" });
})
.addListener("update-not-available", () => {
sendEvent({ type: "update-not-available" });
})
.addListener("update-available", (info: UpdateInfo) => {
sendEvent({ type: "update-available", info });
})
.addListener("update-downloaded", () => {
sendEvent({ type: "update-downloaded" });
})
.addListener("download-progress", (info: ProgressInfo) => {
sendEvent({ type: "download-progress", info });
})
.addListener("update-cancelled", () => {
sendEvent({ type: "update-cancelled" });
});
if (app.isPackaged) {

View file

@ -1,12 +0,0 @@
import { WindowManager } from "@main/services";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
const { autoUpdater } = updater;
const continueToMainWindow = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
};
registerEvent("continueToMainWindow", continueToMainWindow);

View file

@ -1,16 +1,13 @@
import { app } from "electron";
import { registerEvent } from "../register-event";
import updater from "electron-updater";
import { WindowManager } from "@main/services";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
autoUpdater.removeAllListeners();
if (app.isPackaged) {
autoUpdater.quitAndInstall(true, true);
} else {
autoUpdater.removeAllListeners();
WindowManager.prepareMainWindowAndCloseSplash();
}
};

View file

@ -29,7 +29,6 @@ import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./autoupdater/continue-to-main-window";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View file

@ -64,7 +64,7 @@ app.whenReady().then(() => {
where: { id: 1 },
});
WindowManager.createSplashScreen();
WindowManager.createMainWindow();
WindowManager.createSystemTray(userPreferences?.language || "en");
});
});

View file

@ -17,9 +17,6 @@ import { IsNull, Not } from "typeorm";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static splashWindow: Electron.BrowserWindow | null = null;
public static isReadyToShowMainWindow = false;
private static isMainMaximized = false;
private static loadURL(hash = "") {
// HMR for renderer base on electron-vite cli.
@ -38,48 +35,8 @@ export class WindowManager {
}
}
private static loadSplashURL() {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.splashWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/splash`
);
} else {
this.splashWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "splash",
}
);
}
}
public static createSplashScreen() {
if (this.splashWindow) return;
this.splashWindow = new BrowserWindow({
width: 380,
height: 380,
frame: false,
resizable: false,
backgroundColor: "#1c1c1c",
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.loadSplashURL();
this.splashWindow.removeMenu();
if (this.splashWindow?.isMaximized()) {
this.splashWindow?.unmaximize();
this.isMainMaximized = true;
}
}
public static createMainWindow() {
if (this.mainWindow || !this.isReadyToShowMainWindow) return;
if (this.mainWindow) return;
this.mainWindow = new BrowserWindow({
width: 1200,
@ -104,7 +61,6 @@ export class WindowManager {
this.loadURL();
this.mainWindow.removeMenu();
if (this.isMainMaximized) this.mainWindow?.maximize();
this.mainWindow.on("ready-to-show", () => {
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
@ -123,12 +79,6 @@ export class WindowManager {
});
}
public static prepareMainWindowAndCloseSplash() {
this.isReadyToShowMainWindow = true;
this.splashWindow?.close();
this.createMainWindow();
}
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.loadURL(hash);

View file

@ -114,7 +114,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform,
/* Splash */
/* Auto update */
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
@ -129,5 +129,4 @@ contextBridge.exposeInMainWorld("electron", {
},
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
continueToMainWindow: () => ipcRenderer.invoke("continueToMainWindow"),
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View file

@ -145,3 +145,21 @@ export const title = recipe({
},
},
});
export const subheader = style({
borderBottom: `solid 1px ${vars.color.border}`,
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 3}px`,
});
export const newVersionButton = style({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: `${SPACING_UNIT}px`,
color: vars.color.bodyText,
borderBottom: "1px solid transparent",
":hover": {
borderBottom: `1px solid ${vars.color.bodyText}`,
cursor: "pointer",
},
});

View file

@ -1,12 +1,18 @@
import { useTranslation } from "react-i18next";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
import {
ArrowLeftIcon,
SearchIcon,
SyncIcon,
XIcon,
} from "@primer/octicons-react";
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
import * as styles from "./header.css";
import { clearSearch } from "@renderer/features";
import { AppUpdaterEvents } from "@types";
export interface HeaderProps {
onSearch: (query: string) => void;
@ -34,6 +40,9 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
const [isFocused, setIsFocused] = useState(false);
const [showUpdateSubheader, setShowUpdateSubheader] = useState(false);
const [newVersion, setNewVersion] = useState("");
const { t } = useTranslation("header");
const title = useMemo(() => {
@ -49,6 +58,30 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
}
}, [location.pathname, search, dispatch]);
const handleClickRestartAndUpdate = () => {
window.electron.restartAndInstallUpdate();
};
useEffect(() => {
const unsubscribe = window.electron.onAutoUpdaterEvent(
(event: AppUpdaterEvents) => {
if (event.type == "update-available") {
setNewVersion(event.info.version || "");
}
if (event.type == "update-downloaded") {
setShowUpdateSubheader(true);
}
}
);
window.electron.checkForUpdates();
return () => {
unsubscribe();
};
});
const focusInput = () => {
setIsFocused(true);
inputRef.current?.focus();
@ -63,64 +96,80 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
};
return (
<header
className={styles.header({
draggingDisabled,
isWindows: window.electron.platform === "win32",
})}
>
<div className={styles.section}>
<button
type="button"
className={styles.backButton({ enabled: location.key !== "default" })}
onClick={handleBackButtonClick}
disabled={location.key === "default"}
>
<ArrowLeftIcon />
</button>
<h3
className={styles.title({
hasBackButton: location.key !== "default",
})}
>
{title}
</h3>
</div>
<section className={styles.section}>
<div className={styles.search({ focused: isFocused })}>
<>
<header
className={styles.header({
draggingDisabled,
isWindows: window.electron.platform === "win32",
})}
>
<div className={styles.section}>
<button
type="button"
className={styles.actionButton}
onClick={focusInput}
className={styles.backButton({
enabled: location.key !== "default",
})}
onClick={handleBackButtonClick}
disabled={location.key === "default"}
>
<SearchIcon />
<ArrowLeftIcon />
</button>
<input
ref={inputRef}
type="text"
name="search"
placeholder={t("search")}
value={search}
className={styles.searchInput}
onChange={(event) => onSearch(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
/>
<h3
className={styles.title({
hasBackButton: location.key !== "default",
})}
>
{title}
</h3>
</div>
{search && (
<section className={styles.section}>
<div className={styles.search({ focused: isFocused })}>
<button
type="button"
onClick={onClear}
className={styles.actionButton}
onClick={focusInput}
>
<XIcon />
<SearchIcon />
</button>
)}
</div>
</section>
</header>
<input
ref={inputRef}
type="text"
name="search"
placeholder={t("search")}
value={search}
className={styles.searchInput}
onChange={(event) => onSearch(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
/>
{search && (
<button
type="button"
onClick={onClear}
className={styles.actionButton}
>
<XIcon />
</button>
)}
</div>
</section>
</header>
{showUpdateSubheader && (
<header className={styles.subheader}>
<button
type="button"
className={styles.newVersionButton}
onClick={handleClickRestartAndUpdate}
>
<SyncIcon size={12} />
<small>{t("version_available", { version: newVersion })}</small>
</button>
</header>
)}
</>
);
}

View file

@ -92,13 +92,12 @@ declare global {
) => Promise<Electron.OpenDialogReturnValue>;
platform: NodeJS.Platform;
/* Splash */
/* Auto update */
onAutoUpdaterEvent: (
cb: (event: AppUpdaterEvents) => void
) => () => Electron.IpcRenderer;
checkForUpdates: () => Promise<void>;
restartAndInstallUpdate: () => Promise<void>;
continueToMainWindow: () => Promise<void>;
}
interface Window {

View file

@ -27,7 +27,6 @@ import {
import { store } from "./store";
import * as resources from "@locales";
import Splash from "./pages/splash/splash";
i18n
.use(LanguageDetector)
@ -48,7 +47,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<HashRouter>
<Routes>
<Route path="/splash" Component={Splash} />
<Route element={<App />}>
<Route path="/" Component={Home} />
<Route path="/catalogue" Component={Catalogue} />

View file

@ -1,49 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const main = style({
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
padding: `${SPACING_UNIT * 3}px`,
flex: "1",
overflowY: "auto",
alignItems: "center",
});
export const splashIcon = style({
width: "75%",
});
export const updateInfoSection = style({
width: "100%",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
flex: "1",
overflowY: "auto",
alignItems: "center",
justifyContent: "center",
});
export const progressBar = style({
WebkitAppearance: "none",
appearance: "none",
borderRadius: "4px",
width: "100%",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
height: "18px",
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
transition: "width 0.2s",
},
"::-webkit-progress-bar": {
backgroundColor: vars.color.darkBackground,
},
});
export const progressBarText = style({
zIndex: 2,
});

View file

@ -1,82 +0,0 @@
import icon from "@renderer/assets/icon.png";
import * as styles from "./splash.css";
import { themeClass } from "../../theme.css";
import "../../app.css";
import { useEffect, useState } from "react";
import { AppUpdaterEvents } from "@types";
import { useTranslation } from "react-i18next";
document.body.classList.add(themeClass);
export default function Splash() {
const [status, setStatus] = useState<AppUpdaterEvents | null>(null);
const [newVersion, setNewVersion] = useState("");
const { t } = useTranslation("splash");
useEffect(() => {
const unsubscribe = window.electron.onAutoUpdaterEvent(
(event: AppUpdaterEvents) => {
setStatus(event);
switch (event.type) {
case "error":
window.electron.continueToMainWindow();
break;
case "update-available":
setNewVersion(event.info.version);
break;
case "update-cancelled":
window.electron.continueToMainWindow();
break;
case "update-downloaded":
window.electron.restartAndInstallUpdate();
break;
case "update-not-available":
window.electron.continueToMainWindow();
break;
}
}
);
window.electron.checkForUpdates();
return () => {
unsubscribe();
};
}, []);
const renderUpdateInfo = () => {
switch (status?.type) {
case "download-progress":
return (
<>
<p>{t("downloading_version", { version: newVersion })}</p>
<progress
className={styles.progressBar}
max="100"
value={status.info.percent}
/>
</>
);
case "checking-for-updates":
return <p>{t("searching_updates")}</p>;
case "update-available":
return <p>{t("update_found", { version: newVersion })}</p>;
case "update-downloaded":
return <p>{t("restarting_and_applying")}</p>;
default:
return <></>;
}
};
return (
<main className={styles.main}>
<img src={icon} className={styles.splashIcon} alt="Hydra Launcher Logo" />
<section className={styles.updateInfoSection}>
{renderUpdateInfo()}
</section>
</main>
);
}

View file

@ -1,5 +1,5 @@
import type { Downloader, GameStatus } from "@shared";
import { ProgressInfo, UpdateInfo } from "electron-updater";
import { UpdateInfo } from "electron-updater";
export type GameShop = "steam" | "epic";
export type CatalogueCategory = "recently_added" | "trending";
@ -146,10 +146,5 @@ export interface SteamGame {
}
export type AppUpdaterEvents =
| { type: "error" }
| { type: "checking-for-updates" }
| { type: "update-not-available" }
| { type: "update-available"; info: UpdateInfo }
| { type: "update-downloaded" }
| { type: "download-progress"; info: ProgressInfo }
| { type: "update-cancelled" };
| { type: "update-available"; info: Partial<UpdateInfo> }
| { type: "update-downloaded" };