diff --git a/hydra.db b/hydra.db index 4522e1ae..dad57120 100644 Binary files a/hydra.db and b/hydra.db differ diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 7b54b889..1c948ba8 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -176,5 +176,11 @@ }, "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" } } diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 57ec0470..6894fb7c 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -176,5 +176,11 @@ }, "modal": { "close": "Botão de fechar" + }, + "splash": { + "downloading_version": "Baixando versão {{version}}", + "searching_updates": "Buscando atualizações", + "update_found": "Versão {{version}} encontrada", + "restarting_and_applying": "Reiniciando e aplicando atualização" } } diff --git a/src/main/constants.ts b/src/main/constants.ts index 4d43518b..17eea7ca 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -22,7 +22,7 @@ export const defaultDownloadsPath = app.getPath("downloads"); export const databasePath = path.join( app.getPath("appData"), - app.getName(), + "hydra", "hydra.db" ); diff --git a/src/main/events/autoupdater/check-for-updates.ts b/src/main/events/autoupdater/check-for-updates.ts new file mode 100644 index 00000000..aa63575f --- /dev/null +++ b/src/main/events/autoupdater/check-for-updates.ts @@ -0,0 +1,48 @@ +import { AppUpdaterEvents } from "@types"; +import { registerEvent } from "../register-event"; +import updater, { ProgressInfo, 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); +}; + +const mockValuesForDebug = async () => { + 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) { + autoUpdater.checkForUpdates(); + } else { + await mockValuesForDebug(); + } +}; + +registerEvent("checkForUpdates", checkForUpdates); diff --git a/src/main/events/autoupdater/continue-to-main-window.ts b/src/main/events/autoupdater/continue-to-main-window.ts new file mode 100644 index 00000000..6a8965f9 --- /dev/null +++ b/src/main/events/autoupdater/continue-to-main-window.ts @@ -0,0 +1,12 @@ +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); diff --git a/src/main/events/autoupdater/restart-and-install-update.ts b/src/main/events/autoupdater/restart-and-install-update.ts new file mode 100644 index 00000000..be301c18 --- /dev/null +++ b/src/main/events/autoupdater/restart-and-install-update.ts @@ -0,0 +1,17 @@ +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) => { + if (app.isPackaged) { + autoUpdater.quitAndInstall(true, true); + } else { + autoUpdater.removeAllListeners(); + WindowManager.prepareMainWindowAndCloseSplash(); + } +}; + +registerEvent("restartAndInstallUpdate", restartAndInstallUpdate); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 5d721c62..debca0e4 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -27,6 +27,9 @@ import "./torrenting/start-game-download"; import "./user-preferences/get-user-preferences"; 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()); diff --git a/src/main/index.ts b/src/main/index.ts index dabdeb4e..22c13388 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,11 +3,10 @@ import updater from "electron-updater"; import i18n from "i18next"; import path from "node:path"; import { electronApp, optimizer } from "@electron-toolkit/utils"; -import { resolveDatabaseUpdates, WindowManager } from "@main/services"; +import { logger, resolveDatabaseUpdates, WindowManager } from "@main/services"; import { dataSource } from "@main/data-source"; import * as resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; - const { autoUpdater } = updater; autoUpdater.setFeedURL({ @@ -16,6 +15,8 @@ autoUpdater.setFeedURL({ repo: "hydra", }); +autoUpdater.logger = logger; + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) app.quit(); @@ -63,12 +64,8 @@ app.whenReady().then(() => { where: { id: 1 }, }); - WindowManager.createMainWindow(); + WindowManager.createSplashScreen(); WindowManager.createSystemTray(userPreferences?.language || "en"); - - WindowManager.mainWindow?.on("ready-to-show", () => { - autoUpdater.checkForUpdatesAndNotify(); - }); }); }); diff --git a/src/main/services/repack-tracker/online-fix.ts b/src/main/services/repack-tracker/online-fix.ts index 38864a8b..265f6b70 100644 --- a/src/main/services/repack-tracker/online-fix.ts +++ b/src/main/services/repack-tracker/online-fix.ts @@ -148,9 +148,10 @@ export const getNewRepacksFromOnlineFix = async ( ); if (!newRepacks.length) return; - if (page === totalPages) return; await savePage(newRepacks); + if (page === totalPages) return; + return getNewRepacksFromOnlineFix(existingRepacks, page + 1, cookieJar); }; diff --git a/src/main/services/repack-tracker/xatab.ts b/src/main/services/repack-tracker/xatab.ts index 34ebfa4c..e765bebf 100644 --- a/src/main/services/repack-tracker/xatab.ts +++ b/src/main/services/repack-tracker/xatab.ts @@ -111,9 +111,10 @@ export const getNewRepacksFromXatab = async ( ); if (!newRepacks.length) return; - if (page === totalPages) return; await savePage(newRepacks); + if (page === totalPages) return; + return getNewRepacksFromXatab(existingRepacks, page + 1); }; diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 973a0c64..e435ddb2 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -17,6 +17,8 @@ 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 loadURL(hash = "") { // HMR for renderer base on electron-vite cli. @@ -35,13 +37,51 @@ 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(); + } + public static createMainWindow() { - // Create the browser window. + if (this.mainWindow || !this.isReadyToShowMainWindow) return; + this.mainWindow = new BrowserWindow({ width: 1200, height: 720, minWidth: 1024, minHeight: 540, + backgroundColor: "#1c1c1c", titleBarStyle: "hidden", ...(process.platform === "linux" ? { icon } : {}), trafficLightPosition: { x: 16, y: 16 }, @@ -75,6 +115,12 @@ 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); diff --git a/src/preload/index.ts b/src/preload/index.ts index 6a209787..4ddf5009 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7,6 +7,7 @@ import type { GameShop, TorrentProgress, UserPreferences, + AppUpdaterEvents, } from "@types"; contextBridge.exposeInMainWorld("electron", { @@ -112,4 +113,21 @@ contextBridge.exposeInMainWorld("electron", { showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), platform: process.platform, + + /* Splash */ + onAutoUpdaterEvent: (cb: (value: AppUpdaterEvents) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + value: AppUpdaterEvents + ) => cb(value); + + ipcRenderer.on("autoUpdaterEvent", listener); + + return () => { + ipcRenderer.removeListener("autoUpdaterEvent", listener); + }; + }, + checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"), + restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"), + continueToMainWindow: () => ipcRenderer.invoke("continueToMainWindow"), }); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index da95f292..a1dd3d9a 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -12,7 +12,7 @@ import { import * as styles from "./app.css"; import { themeClass } from "./theme.css"; -import { useLocation, useNavigate } from "react-router-dom"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { setSearch, clearSearch, @@ -27,7 +27,7 @@ export interface AppProps { children: React.ReactNode; } -export function App({ children }: AppProps) { +export function App() { const contentRef = useRef(null); const { updateLibrary } = useLibrary(); @@ -128,7 +128,7 @@ export function App({ children }: AppProps) { />
- {children} +
diff --git a/src/renderer/src/assets/icon.png b/src/renderer/src/assets/icon.png new file mode 100644 index 00000000..9254a8fb Binary files /dev/null and b/src/renderer/src/assets/icon.png differ diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 045915bb..608f21a0 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -1,4 +1,5 @@ import type { + AppUpdaterEvents, CatalogueCategory, CatalogueEntry, Game, @@ -90,6 +91,14 @@ declare global { options: Electron.OpenDialogOptions ) => Promise; platform: NodeJS.Platform; + + /* Splash */ + onAutoUpdaterEvent: ( + cb: (event: AppUpdaterEvents) => void + ) => () => Electron.IpcRenderer; + checkForUpdates: () => Promise; + restartAndInstallUpdate: () => Promise; + continueToMainWindow: () => Promise; } interface Window { diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index f44653cb..3608af8d 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -27,6 +27,7 @@ import { import { store } from "./store"; import * as resources from "@locales"; +import Splash from "./pages/splash/splash"; i18n .use(LanguageDetector) @@ -46,16 +47,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - - + + + }> - - + + diff --git a/src/renderer/src/pages/game-details/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider.tsx index 8879b2ff..e1925065 100644 --- a/src/renderer/src/pages/game-details/gallery-slider.tsx +++ b/src/renderer/src/pages/game-details/gallery-slider.tsx @@ -58,9 +58,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) { if (hasMovies && mediaContainerRef.current) { mediaContainerRef.current.childNodes.forEach((node, index) => { if (node instanceof HTMLVideoElement) { - if (index == mediaIndex) { - node.play(); - } else { + if (index !== mediaIndex) { node.pause(); } } diff --git a/src/renderer/src/pages/splash/splash.css.ts b/src/renderer/src/pages/splash/splash.css.ts new file mode 100644 index 00000000..36aacfff --- /dev/null +++ b/src/renderer/src/pages/splash/splash.css.ts @@ -0,0 +1,49 @@ +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, +}); diff --git a/src/renderer/src/pages/splash/splash.tsx b/src/renderer/src/pages/splash/splash.tsx new file mode 100644 index 00000000..dec308c4 --- /dev/null +++ b/src/renderer/src/pages/splash/splash.tsx @@ -0,0 +1,82 @@ +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(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 ( + <> +

{t("downloading_version", { version: newVersion })}

+ + + ); + case "checking-for-updates": + return

{t("searching_updates")}

; + case "update-available": + return

{t("update_found", { version: newVersion })}

; + case "update-downloaded": + return

{t("restarting_and_applying")}

; + default: + return <>; + } + }; + + return ( +
+ Hydra Launcher Logo +
+ {renderUpdateInfo()} +
+
+ ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 7895686d..0abce31f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ import type { Downloader, GameStatus } from "@shared"; +import { ProgressInfo, UpdateInfo } from "electron-updater"; export type GameShop = "steam" | "epic"; export type CatalogueCategory = "recently_added" | "trending"; @@ -143,3 +144,12 @@ export interface SteamGame { name: string; clientIcon: string | null; } + +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" };