mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
23 Commits
github/for
...
thegrannyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8c63aa25 | ||
|
|
be48306ca2 | ||
|
|
ab81e21341 | ||
|
|
b7f94102da | ||
|
|
9e7b27afe6 | ||
|
|
c24523e8e6 | ||
|
|
b58330ed35 | ||
|
|
dde40f39e9 | ||
|
|
d2b3017de9 | ||
|
|
64f4dad7cc | ||
|
|
154d211b21 | ||
|
|
7905ef6c10 | ||
|
|
b09f2c055f | ||
|
|
2c5b3b4ffa | ||
|
|
fdefc0c165 | ||
|
|
47ca2535e3 | ||
|
|
f706836a43 | ||
|
|
d8158bb80e | ||
|
|
4e422bdf91 | ||
|
|
4be3db8007 | ||
|
|
29b64237ed | ||
|
|
d481164bf3 | ||
|
|
dc94a886e6 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.3",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
|
||||
@@ -20,13 +20,7 @@
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (Queued)",
|
||||
"game_has_no_executable": "Game has no executable selected",
|
||||
"sign_in": "Sign in",
|
||||
"sort_by": "Sort by",
|
||||
"latest_added": "Latest added",
|
||||
"alphabetically": "Alphabetically",
|
||||
"last_launched": "Last launched",
|
||||
"number_of_hours": "Number of hours",
|
||||
"installed_or_not": "Installed or not"
|
||||
"sign_in": "Sign in"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@@ -205,7 +199,9 @@
|
||||
"game_ready_to_install": "{{title}} is ready to install",
|
||||
"repack_list_updated": "Repack list updated",
|
||||
"repack_count_one": "{{count}} repack added",
|
||||
"repack_count_other": "{{count}} repacks added"
|
||||
"repack_count_other": "{{count}} repacks added",
|
||||
"new_update_available": "Version {{version}} available",
|
||||
"restart_to_install_update": "Restart Hydra to install the update"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Open Hydra",
|
||||
|
||||
@@ -199,7 +199,8 @@
|
||||
"game_ready_to_install": "{{title}} está listo para instalarse",
|
||||
"repack_list_updated": "Lista de repacks actualizadas",
|
||||
"repack_count_one": "{{count}} repack ha sido añadido",
|
||||
"repack_count_other": "{{count}} repacks añadidos"
|
||||
"repack_count_other": "{{count}} repacks añadidos",
|
||||
"new_update_available": "Version {{version}} disponible"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
|
||||
@@ -20,13 +20,7 @@
|
||||
"home": "Início",
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||
"sign_in": "Login",
|
||||
"sort_by": "Ordenar por",
|
||||
"latest_added": "Adicionado recente",
|
||||
"alphabetically": "Alfabeticamente",
|
||||
"last_launched": "Último jogado",
|
||||
"number_of_hours": "Qtd. de horas jogadas",
|
||||
"installed_or_not": "Instalado ou não"
|
||||
"sign_in": "Login"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
@@ -201,7 +195,9 @@
|
||||
"game_ready_to_install": "{{title}} está pronto para ser instalado",
|
||||
"repack_list_updated": "Lista de repacks atualizada",
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks"
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
|
||||
@@ -197,7 +197,8 @@
|
||||
"game_ready_to_install": "{{title}} готова к установке",
|
||||
"repack_list_updated": "Список репаков обновлен",
|
||||
"repack_count_one": "{{count}} репак добавлен",
|
||||
"repack_count_other": "{{count}} репаков добавлено"
|
||||
"repack_count_other": "{{count}} репаков добавлено",
|
||||
"new_update_available": "Доступна версия {{version}}"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Открыть Hydra",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { registerEvent } from "../register-event";
|
||||
import updater, { UpdateInfo } from "electron-updater";
|
||||
import { WindowManager } from "@main/services";
|
||||
import { app } from "electron";
|
||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
@@ -20,13 +21,17 @@ const mockValuesForDebug = () => {
|
||||
sendEvent({ type: "update-downloaded" });
|
||||
};
|
||||
|
||||
const newVersionInfo = { version: "" };
|
||||
|
||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
autoUpdater
|
||||
.once("update-available", (info: UpdateInfo) => {
|
||||
sendEvent({ type: "update-available", info });
|
||||
newVersionInfo.version = info.version;
|
||||
})
|
||||
.once("update-downloaded", () => {
|
||||
sendEvent({ type: "update-downloaded" });
|
||||
publishNotificationUpdateReadyToInstall(newVersionInfo.version);
|
||||
});
|
||||
|
||||
if (app.isPackaged) {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import axios from "axios";
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { downloadSourceSchema } from "../helpers/validators";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { downloadSourceWorker } from "@main/workers";
|
||||
|
||||
const validateDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const existingSource = await downloadSourceRepository.findOne({
|
||||
where: { url },
|
||||
});
|
||||
@@ -21,14 +16,12 @@ const validateDownloadSource = async (
|
||||
|
||||
const repacks = RepacksManager.repacks;
|
||||
|
||||
const existingUris = source.downloads
|
||||
.flatMap((download) => download.uris)
|
||||
.filter((uri) => repacks.some((repack) => repack.magnet === uri));
|
||||
|
||||
return {
|
||||
name: source.name,
|
||||
downloadCount: source.downloads.length - existingUris.length,
|
||||
};
|
||||
return downloadSourceWorker.run(
|
||||
{ url, repacks },
|
||||
{
|
||||
name: "validateDownloadSource",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import AutoLaunch from "auto-launch";
|
||||
import { app } from "electron";
|
||||
import path from "path";
|
||||
import fs from "node:fs";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
const windowsStartupPath = path.join(
|
||||
app.getPath("appData"),
|
||||
"Microsoft",
|
||||
"Windows",
|
||||
"Start Menu",
|
||||
"Programs",
|
||||
"Startup"
|
||||
);
|
||||
|
||||
const autoLaunch = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -13,9 +25,17 @@ const autoLaunch = async (
|
||||
});
|
||||
|
||||
if (enabled) {
|
||||
appLauncher.enable().catch(() => {});
|
||||
appLauncher.enable().catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
} else {
|
||||
appLauncher.disable().catch(() => {});
|
||||
if (process.platform == "win32") {
|
||||
fs.rm(path.join(windowsStartupPath, "Hydra.vbs"), () => {});
|
||||
}
|
||||
|
||||
appLauncher.disable().catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
|
||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
await RepacksManager.updateRepacks();
|
||||
RepacksManager.updateRepacks();
|
||||
|
||||
import("./events");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Notification, nativeImage } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { parseICO } from "icojs";
|
||||
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { Game } from "@main/entity";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
|
||||
@@ -39,11 +39,9 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
title: game.title,
|
||||
}),
|
||||
icon,
|
||||
@@ -60,13 +58,26 @@ export const publishNewRepacksNotifications = async (count: number) => {
|
||||
new Notification({
|
||||
title: t("repack_list_updated", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences?.language || "en",
|
||||
}),
|
||||
body: t("repack_count", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences?.language || "en",
|
||||
count: count,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
|
||||
export const publishNotificationUpdateReadyToInstall = async (
|
||||
version: string
|
||||
) => {
|
||||
new Notification({
|
||||
title: t("new_update_available", {
|
||||
ns: "notifications",
|
||||
version,
|
||||
}),
|
||||
body: t("restart_to_install_update", {
|
||||
ns: "notifications",
|
||||
}),
|
||||
icon: trayIcon,
|
||||
}).show();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import type { DownloadSource } from "@types";
|
||||
import type { DownloadSource, GameRepack } from "@types";
|
||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -48,3 +48,24 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const validateDownloadSource = async ({
|
||||
url,
|
||||
repacks,
|
||||
}: {
|
||||
url: string;
|
||||
repacks: GameRepack[];
|
||||
}) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const existingUris = source.downloads
|
||||
.flatMap((download) => download.uris)
|
||||
.filter((uri) => repacks.some((repack) => repack.magnet === uri));
|
||||
|
||||
return {
|
||||
name: source.name,
|
||||
downloadCount: source.downloads.length - existingUris.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -47,10 +47,8 @@ export function AutoUpdateSubHeader() {
|
||||
return (
|
||||
<header className={styles.subheader}>
|
||||
<Link to={releasesPageUrl} className={styles.newVersionLink}>
|
||||
<SyncIcon size={12} />
|
||||
<small>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
{t("version_available_download", { version: newVersion })}
|
||||
</small>
|
||||
</Link>
|
||||
</header>
|
||||
);
|
||||
@@ -64,10 +62,8 @@ export function AutoUpdateSubHeader() {
|
||||
className={styles.newVersionButton}
|
||||
onClick={handleClickInstallUpdate}
|
||||
>
|
||||
<SyncIcon size={12} />
|
||||
<small>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
{t("version_available_install", { version: newVersion })}
|
||||
</small>
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -157,7 +157,7 @@ export const newVersionButton = style({
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.body,
|
||||
fontSize: "13px",
|
||||
fontSize: "12px",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
@@ -169,5 +169,9 @@ export const newVersionLink = style({
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: "#8e919b",
|
||||
fontSize: "13px",
|
||||
fontSize: "12px",
|
||||
});
|
||||
|
||||
export const newVersionIcon = style({
|
||||
color: vars.color.success,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export const select = recipe({
|
||||
base: {
|
||||
display: "inline-flex",
|
||||
transition: "all ease 0.2s",
|
||||
width: "100%",
|
||||
width: "fit-content",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
|
||||
@@ -34,7 +34,6 @@ export function SelectField({
|
||||
<select
|
||||
id={id}
|
||||
value={value}
|
||||
style={{width: "100%"}}
|
||||
className={styles.option}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
import { SelectField, TextField } from "@renderer/components";
|
||||
import { TextField } from "@renderer/components";
|
||||
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||
|
||||
import { routes } from "./routes";
|
||||
@@ -34,43 +34,11 @@ export function Sidebar() {
|
||||
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
|
||||
);
|
||||
|
||||
const [sortParam, setSortParam] = useState<string>("latest_added");
|
||||
const sortParamOptions = [
|
||||
"latest_added",
|
||||
"alphabetically",
|
||||
"last_launched",
|
||||
"number_of_hours",
|
||||
"installed_or_not",
|
||||
];
|
||||
|
||||
const handleSortParamChange = (e) => {
|
||||
const selectedOption: string = e.target.value;
|
||||
setSortParam(selectedOption);
|
||||
};
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const sortedLibrary = useMemo(() => {
|
||||
switch (sortParam) {
|
||||
case "latest_added":
|
||||
return sortBy(library, (game) => game.createdAt);
|
||||
break;
|
||||
case "alphabetically":
|
||||
return sortBy(library, (game) => game.title);
|
||||
break;
|
||||
case "last_launched":
|
||||
return sortBy(library, (game) => game.lastTimePlayed);
|
||||
break;
|
||||
case "number_of_hours":
|
||||
return sortBy(library, (game) => game.playTimeInMilliseconds);
|
||||
break;
|
||||
case "installed_or_not":
|
||||
return sortBy(library, (game) => game.executablePath !== null);
|
||||
break;
|
||||
default:
|
||||
return sortBy(library, (game) => game.title);
|
||||
}
|
||||
}, [library, sortParam]);
|
||||
return sortBy(library, (game) => game.title);
|
||||
}, [library]);
|
||||
|
||||
const { lastPacket, progress } = useDownload();
|
||||
|
||||
@@ -225,19 +193,6 @@ export function Sidebar() {
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
|
||||
|
||||
<div style={{width: "102%"}}>
|
||||
<SelectField
|
||||
label={t("sort_by")}
|
||||
value={sortParam}
|
||||
onChange={handleSortParamChange}
|
||||
options={sortParamOptions.map((option) => ({
|
||||
key: option,
|
||||
value: option,
|
||||
label: t(option),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
placeholder={t("filter")}
|
||||
onChange={handleFilter}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import parseTorrent from "parse-torrent";
|
||||
|
||||
@@ -12,6 +12,7 @@ import { format } from "date-fns";
|
||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { Downloader } from "@shared";
|
||||
import { orderBy } from "lodash-es";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
visible: boolean;
|
||||
@@ -38,16 +39,20 @@ export function RepacksModal({
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const sortedRepacks = useMemo(() => {
|
||||
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
||||
}, [repacks]);
|
||||
|
||||
const getInfoHash = useCallback(async () => {
|
||||
const torrent = await parseTorrent(game?.uri ?? "");
|
||||
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
||||
}, [game]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredRepacks(repacks);
|
||||
setFilteredRepacks(sortedRepacks);
|
||||
|
||||
if (game?.uri) getInfoHash();
|
||||
}, [repacks, visible, game, getInfoHash]);
|
||||
}, [sortedRepacks, visible, game, getInfoHash]);
|
||||
|
||||
const handleRepackClick = (repack: GameRepack) => {
|
||||
setRepack(repack);
|
||||
@@ -58,7 +63,7 @@ export function RepacksModal({
|
||||
const term = event.target.value.toLocaleLowerCase();
|
||||
|
||||
setFilteredRepacks(
|
||||
repacks.filter((repack) => {
|
||||
sortedRepacks.filter((repack) => {
|
||||
const lowerCaseTitle = repack.title.toLowerCase();
|
||||
const lowerCaseRepacker = repack.repacker.toLowerCase();
|
||||
|
||||
|
||||
@@ -121,19 +121,16 @@ export function SettingsGeneral() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div style={{width: "22%"}}>
|
||||
<SelectField
|
||||
label={t("language")}
|
||||
value={form.language}
|
||||
onChange={handleLanguageChange}
|
||||
options={languageOptions.map((language) => ({
|
||||
key: language.option,
|
||||
value: language.option,
|
||||
label: language.nativeName,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SelectField
|
||||
label={t("language")}
|
||||
value={form.language}
|
||||
onChange={handleLanguageChange}
|
||||
options={languageOptions.map((language) => ({
|
||||
key: language.option,
|
||||
value: language.option,
|
||||
label: language.nativeName,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<h3>{t("notifications")}</h3>
|
||||
<>
|
||||
|
||||
@@ -51,10 +51,15 @@ export const removeSpecialEditionFromName = (name: string) =>
|
||||
export const removeDuplicateSpaces = (name: string) =>
|
||||
name.replace(/\s{2,}/g, " ");
|
||||
|
||||
export const replaceUnderscoreWithSpace = (name: string) =>
|
||||
name.replace(/_/g, " ");
|
||||
|
||||
export const formatName = pipe<string>(
|
||||
removeReleaseYearFromName,
|
||||
removeSymbolsFromName,
|
||||
removeSpecialEditionFromName,
|
||||
replaceUnderscoreWithSpace,
|
||||
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
|
||||
removeSymbolsFromName,
|
||||
removeDuplicateSpaces,
|
||||
(str) => str.trim()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user