mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: correcting date in process-watcher
This commit is contained in:
commit
85516c1744
47 changed files with 334 additions and 489 deletions
|
@ -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",
|
||||
|
@ -153,6 +154,7 @@
|
|||
"real_debrid": "Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
|
||||
"real_debrid_api_token_hint": "You can get your API token <0>here</0>.",
|
||||
"real_debrid_free_account": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid.",
|
||||
"save_changes": "Save changes"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -176,11 +178,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"catalogue": "Catálogo",
|
||||
"downloads": "Descargas",
|
||||
"search_results": "Resultados de búsqueda",
|
||||
"settings": "Ajustes"
|
||||
"settings": "Ajustes",
|
||||
"version_available": "Version {{version}} disponible. Haga clic aquí para reiniciar e instalar."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||
|
@ -54,7 +55,7 @@
|
|||
"remove": "Eliminar",
|
||||
"remove_from_list": "Quitar",
|
||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
||||
"eta": "Finalizando en {{eta}}",
|
||||
"eta": "Tiempo restante: {{eta}}",
|
||||
"downloading_metadata": "Descargando metadatos…",
|
||||
"checking_files": "Analizando archivos…",
|
||||
"filter": "Buscar repacks",
|
||||
|
@ -176,11 +177,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
"minutes": "minuti",
|
||||
"amount_hours": "{{amount}} ore",
|
||||
"amount_minutes": "{{amount}} minuti",
|
||||
"accuracy": "{{accuratezza}}% di accuratezza",
|
||||
"accuracy": "{{accuracy}}% di accuratezza",
|
||||
"add_to_library": "Aggiungi alla libreria",
|
||||
"remove_from_library": "Rimuovi dalla libreria",
|
||||
"no_downloads": "Nessun download disponibile",
|
||||
|
@ -86,7 +86,6 @@
|
|||
"playing_now": "Stai giocando adesso",
|
||||
"change": "Aggiorna",
|
||||
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
|
||||
"downloads_path": "Percorso dei download",
|
||||
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
|
||||
"download_now": "Scarica ora",
|
||||
"installation_instructions": "Istruzioni di installazione",
|
||||
|
@ -96,7 +95,14 @@
|
|||
"dont_show_it_again": "Non mostrarlo più",
|
||||
"copy_to_clipboard": "Copia",
|
||||
"copied_to_clipboard": "Copiato",
|
||||
"got_it": "Capito"
|
||||
"got_it": "Capito",
|
||||
"no_shop_details": "Impossibile recuperare i dettagli del negozio.",
|
||||
"download_options": "Opzioni di download",
|
||||
"download_path": "Percorso di download",
|
||||
"previous_screenshot": "Screenshot precedente",
|
||||
"next_screenshot": "Screenshot successivo",
|
||||
"screenshot": "Screenshot {{number}}",
|
||||
"open_screenshot": "Apri screenshot {{number}}"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Attiva Hydra",
|
||||
|
@ -127,7 +133,9 @@
|
|||
"remove_from_list": "Rimuovi",
|
||||
"delete_modal_title": "Sei sicuro?",
|
||||
"delete_modal_description": "Questo rimuoverà tutti i file di installazione dal tuo computer",
|
||||
"install": "Installa"
|
||||
"install": "Installa",
|
||||
"real_debrid": "Real Debrid",
|
||||
"torrent": "Torrent"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Percorso dei download",
|
||||
|
@ -136,7 +144,16 @@
|
|||
"enable_download_notifications": "Quando un download è completo",
|
||||
"enable_repack_list_notifications": "Quando viene aggiunto un nuovo repack",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Abilita statistiche di utilizzo anonime"
|
||||
"telemetry_description": "Abilita statistiche di utilizzo anonime",
|
||||
"real_debrid_api_token_label": "Token API Real Debrid",
|
||||
"quit_app_instead_hiding": "Esci da Hydra invece di nascondere nell'area di notifica",
|
||||
"launch_with_system": "Apri Hydra all'avvio",
|
||||
"general": "Generale",
|
||||
"behavior": "Comportamento",
|
||||
"enable_real_debrid": "Abilita Real Debrid",
|
||||
"real_debrid": "Real Debrid",
|
||||
"real_debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>.",
|
||||
"save_changes": "Salva modifiche"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download completato",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -26,6 +26,8 @@ export const databasePath = path.join(
|
|||
"hydra.db"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
||||
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
|
|
|
@ -57,7 +57,7 @@ export class Game {
|
|||
@Column("int", { default: 0 })
|
||||
bytesDownloaded: number;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
@Column("datetime", { nullable: true })
|
||||
lastTimePlayed: Date | null;
|
||||
|
||||
@Column("float", { default: 0 })
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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";
|
||||
import "./user-preferences/authenticate-real-debrid";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
|
|
|
@ -50,8 +50,6 @@ const startGameDownload = async (
|
|||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
|
||||
return DownloadManager.startDownload(game);
|
||||
} else {
|
||||
const steamGame = stateManager
|
||||
.getValue("steamGames")
|
||||
|
@ -81,16 +79,16 @@ const startGameDownload = async (
|
|||
|
||||
return result;
|
||||
});
|
||||
|
||||
const createdGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
return DownloadManager.startDownload(createdGame!);
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
};
|
||||
|
||||
registerEvent("startGameDownload", startGameDownload);
|
||||
|
|
|
@ -64,7 +64,7 @@ app.whenReady().then(() => {
|
|||
where: { id: 1 },
|
||||
});
|
||||
|
||||
WindowManager.createSplashScreen();
|
||||
WindowManager.createMainWindow();
|
||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { Game } from "@main/entity";
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AlterLastTimePlayedToDatime1716776027208
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 2024-05-27 02:08:17
|
||||
// Mon, 27 May 2024 02:08:17 GMT
|
||||
const updateLastTimePlayedValues = `
|
||||
UPDATE game SET lastTimePlayed = (SELECT
|
||||
SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year
|
||||
CASE SUBSTR(lastTimePlayed, 9, 3)
|
||||
WHEN 'Jan' THEN '01'
|
||||
WHEN 'Feb' THEN '02'
|
||||
WHEN 'Mar' THEN '03'
|
||||
WHEN 'Apr' THEN '04'
|
||||
WHEN 'May' THEN '05'
|
||||
WHEN 'Jun' THEN '06'
|
||||
WHEN 'Jul' THEN '07'
|
||||
WHEN 'Aug' THEN '08'
|
||||
WHEN 'Sep' THEN '09'
|
||||
WHEN 'Oct' THEN '10'
|
||||
WHEN 'Nov' THEN '11'
|
||||
WHEN 'Dec' THEN '12'
|
||||
END || '-' || -- Month
|
||||
SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day
|
||||
SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss;
|
||||
FROM game)
|
||||
WHERE lastTimePlayed IS NOT NULL;
|
||||
`;
|
||||
|
||||
await queryRunner.query(updateLastTimePlayedValues);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game");
|
||||
|
||||
const result = await queryBuilder.getMany();
|
||||
|
||||
for (const game of result) {
|
||||
if (!game.lastTimePlayed) continue;
|
||||
await queryRunner.query(
|
||||
`UPDATE game set lastTimePlayed = ? WHERE id = ?;`,
|
||||
[game.lastTimePlayed.toUTCString(), game.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate";
|
||||
import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime";
|
||||
|
||||
export default [FixRepackUploadDate1715900413313];
|
||||
export default [
|
||||
FixRepackUploadDate1715900413313,
|
||||
AlterLastTimePlayedToDatime1716776027208,
|
||||
];
|
||||
|
|
|
@ -186,7 +186,8 @@ export class DownloadManager {
|
|||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
if (!isNaN(progress))
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
const payload = {
|
||||
numPeers: Number(status.connections),
|
||||
|
@ -237,11 +238,12 @@ export class DownloadManager {
|
|||
if (this.gid) {
|
||||
await this.aria2.call("forcePause", this.gid);
|
||||
this.gid = null;
|
||||
this.game = null;
|
||||
this.realDebridTorrentId = null;
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
}
|
||||
|
||||
this.game = null;
|
||||
this.realDebridTorrentId = null;
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
}
|
||||
|
||||
static async resumeDownload(game: Game) {
|
||||
|
@ -251,6 +253,7 @@ export class DownloadManager {
|
|||
|
||||
this.gid = gid;
|
||||
this.game = game;
|
||||
this.realDebridTorrentId = null;
|
||||
} else {
|
||||
return this.startDownload(game);
|
||||
}
|
||||
|
@ -269,7 +272,6 @@ export class DownloadManager {
|
|||
);
|
||||
} else {
|
||||
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
|
||||
|
||||
this.downloads.set(game.id, this.gid);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
import winston from "winston";
|
||||
import { logsPath } from "@main/constants";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: "error.log", level: "error" }),
|
||||
new winston.transports.File({ filename: "info.log", level: "info" }),
|
||||
new winston.transports.File({ filename: "combined.log" }),
|
||||
],
|
||||
log.transports.file.resolvePathFn = (
|
||||
_: log.PathVariables,
|
||||
message?: log.LogMessage | undefined
|
||||
) => {
|
||||
if (message?.level === "error") {
|
||||
return path.join(logsPath, "error.txt");
|
||||
}
|
||||
|
||||
if (message?.level === "info") {
|
||||
return path.join(logsPath, "info.txt");
|
||||
}
|
||||
|
||||
return path.join(logsPath, "logs.txt");
|
||||
};
|
||||
|
||||
log.errorHandler.startCatching({
|
||||
showDialog: false,
|
||||
});
|
||||
|
||||
log.initialize();
|
||||
|
||||
export const logger = log.scope("main");
|
||||
|
|
|
@ -46,7 +46,7 @@ export const watchProcesses = async () => {
|
|||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date().toUTCString(),
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +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 loadURL(hash = "") {
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
|
@ -37,44 +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();
|
||||
}
|
||||
|
||||
public static createMainWindow() {
|
||||
if (this.mainWindow || !this.isReadyToShowMainWindow) return;
|
||||
if (this.mainWindow) return;
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
|
@ -94,6 +56,7 @@ export class WindowManager {
|
|||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
this.loadURL();
|
||||
|
@ -101,6 +64,7 @@ export class WindowManager {
|
|||
|
||||
this.mainWindow.on("ready-to-show", () => {
|
||||
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
|
||||
WindowManager.mainWindow?.show();
|
||||
});
|
||||
|
||||
this.mainWindow.on("close", async () => {
|
||||
|
@ -115,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);
|
||||
|
|
|
@ -104,7 +104,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.invoke("showOpenDialog", options),
|
||||
platform: process.platform,
|
||||
|
||||
/* Splash */
|
||||
/* Auto update */
|
||||
onAutoUpdaterEvent: (cb: (value: AppUpdaterEvent) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
|
@ -119,5 +119,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 |
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { keyframes, style } from "@vanilla-extract/css";
|
|||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
const TOAST_HEIGHT = 55;
|
||||
const TOAST_HEIGHT = 80;
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
|
||||
|
@ -19,12 +19,12 @@ export const toast = recipe({
|
|||
base: {
|
||||
animationDuration: "0.2s",
|
||||
animationTimingFunction: "ease-in-out",
|
||||
height: TOAST_HEIGHT,
|
||||
maxHeight: TOAST_HEIGHT,
|
||||
position: "fixed",
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
left: "50%",
|
||||
right: `${SPACING_UNIT * 2}px`,
|
||||
/* Bottom panel height + 16px */
|
||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
||||
overflow: "hidden",
|
||||
|
@ -32,6 +32,7 @@ export const toast = recipe({
|
|||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
zIndex: "0",
|
||||
maxWidth: "500px",
|
||||
},
|
||||
variants: {
|
||||
closing: {
|
||||
|
|
|
@ -82,6 +82,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
|||
{type === "success" && (
|
||||
<CheckCircleFillIcon className={styles.successIcon} />
|
||||
)}
|
||||
|
||||
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
||||
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||
</div>
|
||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -11,6 +11,7 @@ import type {
|
|||
DownloadProgress,
|
||||
UserPreferences,
|
||||
StartGameDownloadPayload,
|
||||
RealDebridUser,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
|
@ -88,13 +89,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 {
|
||||
|
|
|
@ -20,6 +20,7 @@ export const toastSlice = createSlice({
|
|||
reducers: {
|
||||
showToast: (state, action: PayloadAction<Omit<ToastState, "visible">>) => {
|
||||
state.message = action.payload.message;
|
||||
state.type = action.payload.type;
|
||||
state.visible = true;
|
||||
},
|
||||
closeToast: (state) => {
|
||||
|
|
3
src/renderer/src/logger/index.ts
Normal file
3
src/renderer/src/logger/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import log from "electron-log/renderer";
|
||||
|
||||
export const logger = log.scope("renderer");
|
|
@ -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} />
|
||||
|
|
|
@ -8,6 +8,7 @@ import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
|||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import type { GameRepack, UserPreferences } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface SelectFolderModalProps {
|
||||
visible: boolean;
|
||||
|
@ -95,7 +96,14 @@ export function SelectFolderModal({
|
|||
>
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<label style={{ marginBottom: 0, padding: 0 }}>Method</label>
|
||||
<span
|
||||
style={{
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
Method
|
||||
</span>
|
||||
|
||||
<div className={styles.downloaders}>
|
||||
<Button
|
||||
|
@ -130,25 +138,27 @@ export function SelectFolderModal({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.downloadsPathField}>
|
||||
<TextField value={selectedPath} readOnly disabled label="Path" />
|
||||
<div>
|
||||
<div className={styles.downloadsPathField}>
|
||||
<TextField value={selectedPath} readOnly disabled label="Path" />
|
||||
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
disabled={downloadStarting}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
disabled={downloadStarting}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className={styles.hintText}>
|
||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||
<Link to="/settings" />
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className={styles.hintText}>
|
||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||
<Link to="/settings" />
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button onClick={handleStartClick} disabled={downloadStarting}>
|
||||
<DownloadIcon />
|
||||
{t("download_now")}
|
||||
|
|
|
@ -10,5 +10,5 @@ export const form = style({
|
|||
|
||||
export const description = style({
|
||||
fontFamily: "'Fira Sans', sans-serif",
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
|
|
@ -41,12 +41,7 @@ export function SettingsRealDebrid({
|
|||
event
|
||||
) => {
|
||||
event.preventDefault();
|
||||
dispatch(
|
||||
showToast({
|
||||
message: t("real_debrid_authenticated"),
|
||||
type: "success",
|
||||
})
|
||||
);
|
||||
|
||||
if (form.useRealDebrid) {
|
||||
const user = await window.electron.authenticateRealDebrid(
|
||||
form.realDebridApiToken!
|
||||
|
@ -55,18 +50,25 @@ export function SettingsRealDebrid({
|
|||
if (user.type === "premium") {
|
||||
dispatch(
|
||||
showToast({
|
||||
message: t("real_debrid_authenticated"),
|
||||
type: "success",
|
||||
message: t("real_debrid_free_account", { username: user.username }),
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
|
||||
updateUserPreferences({
|
||||
realDebridApiToken: form.useRealDebrid
|
||||
? form.realDebridApiToken
|
||||
: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch(
|
||||
// showToast({
|
||||
// message: t("real_debrid_free_account", { username: "doctorp" }),
|
||||
// type: "error",
|
||||
// })
|
||||
// );
|
||||
|
||||
updateUserPreferences({
|
||||
realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
|
||||
});
|
||||
};
|
||||
|
||||
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
|
||||
|
@ -106,7 +108,7 @@ export function SettingsRealDebrid({
|
|||
|
||||
<Button
|
||||
type="submit"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{t("save_changes")}
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -230,3 +230,6 @@ export interface RealDebridUser {
|
|||
premium: number;
|
||||
expiration: string;
|
||||
}
|
||||
export type AppUpdaterEvents =
|
||||
| { type: "update-available"; info: Partial<UpdateInfo> }
|
||||
| { type: "update-downloaded" };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue