mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-13 03:32:13 +00:00
feat: moving notifications
This commit is contained in:
parent
d6e57c20c7
commit
ef036d6f57
17 changed files with 179 additions and 87 deletions
|
@ -163,16 +163,20 @@
|
|||
"validate_download_source": "Validate",
|
||||
"remove_download_source": "Remove",
|
||||
"add_download_source": "Add source",
|
||||
"download_options_zero": "No download option",
|
||||
"download_options_one": "{{countFormatted}} download option",
|
||||
"download_options_other": "{{countFormatted}} download options",
|
||||
"download_count_zero": "No downloads in list",
|
||||
"download_count_one": "{{countFormatted}} download in list",
|
||||
"download_count_other": "{{countFormatted}} downloads in list",
|
||||
"download_options_zero": "No download available",
|
||||
"download_options_one": "{{countFormatted}} download available",
|
||||
"download_options_other": "{{countFormatted}} downloads available",
|
||||
"download_source_url": "Download source URL",
|
||||
"add_download_source_description": "Insert the URL containing the JSON file",
|
||||
"add_download_source_description": "Insert the URL containing the .json file",
|
||||
"download_source_up_to_date": "Up-to-date",
|
||||
"download_source_errored": "Errored",
|
||||
"resync_download_sources": "Resync",
|
||||
"sync_download_sources": "Sync sources",
|
||||
"removed_download_source": "Download source removed",
|
||||
"added_download_source": "Added download source"
|
||||
"added_download_source": "Added download source",
|
||||
"download_sources_synced": "All download sources are synced"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||
"general": "Geral",
|
||||
"behavior": "Comportamento",
|
||||
"download_sources": "Bibliotecas de download",
|
||||
"download_sources": "Fontes de download",
|
||||
"language": "Idioma",
|
||||
"real_debrid_api_token": "Token de API",
|
||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||
|
@ -156,20 +156,24 @@
|
|||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||
"save_changes": "Salvar mudanças",
|
||||
"changes_saved": "Ajustes salvos com sucesso",
|
||||
"download_sources_description": "Hydra vai buscar links de download em todas as bibliotecas habilitadas. A URL da biblioteca deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||
"download_sources_description": "Hydra vai buscar links de download em todas as fonte habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||
"validate_download_source": "Validar",
|
||||
"remove_download_source": "Remover",
|
||||
"add_download_source": "Adicionar biblioteca",
|
||||
"download_options_zero": "Sem opções de download",
|
||||
"download_options_one": "{{countFormatted}} opcão de download",
|
||||
"download_options_other": "{{countFormatted}} opções de download",
|
||||
"download_source_url": "URL da biblioteca",
|
||||
"add_download_source_description": "Insira a URL contendo o arquivo JSON",
|
||||
"download_source_up_to_date": "Atualizado",
|
||||
"add_download_source": "Adicionar fonte",
|
||||
"download_count_zero": "Sem downloads na lista",
|
||||
"download_count_one": "{{countFormatted}} download na lista",
|
||||
"download_count_other": "{{countFormatted}} downloads na lista",
|
||||
"download_options_zero": "Sem downloads disponíveis",
|
||||
"download_options_one": "{{countFormatted}} download disponível",
|
||||
"download_options_other": "{{countFormatted}} downloads disponíveis",
|
||||
"download_source_url": "URL da fonte",
|
||||
"add_download_source_description": "Insira a URL contendo o arquivo .json",
|
||||
"download_source_up_to_date": "Sincronizada",
|
||||
"download_source_errored": "Falhou",
|
||||
"resync_download_sources": "Resincronizar",
|
||||
"removed_download_source": "Biblioteca removida",
|
||||
"added_download_source": "Biblioteca adicionada"
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
|
|
|
@ -24,6 +24,9 @@ export class DownloadSource {
|
|||
@Column("text", { nullable: true })
|
||||
etag: string | null;
|
||||
|
||||
@Column("int", { default: 0 })
|
||||
downloadCount: number;
|
||||
|
||||
@Column("text", { default: DownloadSourceStatus.UpToDate })
|
||||
status: DownloadSourceStatus;
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ const addDownloadSource = async (
|
|||
async (transactionalEntityManager) => {
|
||||
const downloadSource = await transactionalEntityManager
|
||||
.getRepository(DownloadSource)
|
||||
.save({ url, name: source.name });
|
||||
.save({
|
||||
url,
|
||||
name: source.name,
|
||||
downloadCount: source.downloads.length,
|
||||
});
|
||||
|
||||
await insertDownloadsFromSource(
|
||||
transactionalEntityManager,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { registerEvent } from "../register-event";
|
||||
import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
|
||||
|
||||
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
fetchDownloadSourcesAndUpdate();
|
||||
|
||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
|
@ -34,6 +34,7 @@ import "./download-sources/get-download-sources";
|
|||
import "./download-sources/validate-download-source";
|
||||
import "./download-sources/add-download-source";
|
||||
import "./download-sources/remove-download-source";
|
||||
import "./download-sources/sync-download-sources";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
|
|
|
@ -52,11 +52,13 @@ export const fetchDownloadSourcesAndUpdate = async () => {
|
|||
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
for (const result of results) {
|
||||
if (result.etag !== null) {
|
||||
await transactionalEntityManager.getRepository(DownloadSource).update(
|
||||
{ id: result.id },
|
||||
{
|
||||
etag: result.etag,
|
||||
status: result.status,
|
||||
downloadCount: result.downloads.length,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -66,6 +68,7 @@ export const fetchDownloadSourcesAndUpdate = async () => {
|
|||
result.downloads
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await RepacksManager.updateRepacks();
|
||||
});
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import Aria2, { StatusResponse } from "aria2";
|
||||
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
||||
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import { Notification } from "electron";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { Downloader } from "@shared";
|
||||
import { DownloadProgress } from "@types";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
|
@ -18,6 +13,7 @@ import { startAria2 } from "./aria2c";
|
|||
import { sleep } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { publishDownloadCompleteNotification } from "./notifications";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloads = new Map<number, string>();
|
||||
|
@ -69,26 +65,6 @@ export class DownloadManager {
|
|||
return -1;
|
||||
}
|
||||
|
||||
static async publishNotification() {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled && this.game) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
title: this.game.title,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static getFolderName(status: StatusResponse) {
|
||||
if (status.bittorrent?.info) return status.bittorrent.info.name;
|
||||
return "";
|
||||
|
@ -222,7 +198,7 @@ export class DownloadManager {
|
|||
}
|
||||
|
||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
await publishDownloadCompleteNotification(this.game);
|
||||
|
||||
await downloadQueueRepository.delete({ game: this.game });
|
||||
|
||||
|
|
24
src/main/services/notifications.ts
Normal file
24
src/main/services/notifications.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Notification } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { Game } from "@main/entity";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
|
||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
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,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
};
|
|
@ -38,6 +38,7 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
|
|||
results.push({
|
||||
...downloadSource,
|
||||
downloads: [],
|
||||
etag: null,
|
||||
status: isNotModified
|
||||
? DownloadSourceStatus.UpToDate
|
||||
: DownloadSourceStatus.Errored,
|
||||
|
|
|
@ -58,6 +58,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.invoke("addDownloadSource", url),
|
||||
removeDownloadSource: (id: number) =>
|
||||
ipcRenderer.invoke("removeDownloadSource", id),
|
||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
|
|
|
@ -103,7 +103,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||
isWindows: window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
<div className={styles.section}>
|
||||
<section className={styles.section}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backButton({
|
||||
|
@ -122,7 +122,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.search({ focused: isFocused })}>
|
||||
|
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
|
@ -85,6 +85,7 @@ declare global {
|
|||
) => Promise<{ name: string; downloadCount: number }>;
|
||||
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
||||
removeDownloadSource: (id: number) => Promise<void>;
|
||||
syncDownloadSources: () => Promise<void>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
|
|
|
@ -48,13 +48,16 @@ export function Downloads() {
|
|||
};
|
||||
|
||||
const result = library.reduce((prev, next) => {
|
||||
if (lastPacket?.game.id === next.id) {
|
||||
return { ...prev, downloading: [...prev.downloading, next] };
|
||||
}
|
||||
/* Game has been manually added to the library */
|
||||
if (!next.status) return prev;
|
||||
|
||||
if (next.downloadQueue || next.status === "paused") {
|
||||
/* Is downloading */
|
||||
if (lastPacket?.game.id === next.id)
|
||||
return { ...prev, downloading: [...prev.downloading, next] };
|
||||
|
||||
/* Is either queued or paused */
|
||||
if (next.downloadQueue || next.status === "paused")
|
||||
return { ...prev, queued: [...prev.queued, next] };
|
||||
}
|
||||
|
||||
return { ...prev, complete: [...prev.complete, next] };
|
||||
}, initialValue);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const downloadSourceField = style({
|
||||
display: "flex",
|
||||
|
@ -14,7 +15,8 @@ export const downloadSources = style({
|
|||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const downloadSourceItem = style({
|
||||
export const downloadSourceItem = recipe({
|
||||
base: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
|
@ -22,6 +24,15 @@ export const downloadSourceItem = style({
|
|||
padding: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
transition: "all ease 0.2s",
|
||||
},
|
||||
variants: {
|
||||
isSyncing: {
|
||||
true: {
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const downloadSourceItemHeader = style({
|
||||
|
@ -36,3 +47,10 @@ export const downloadSourcesHeader = style({
|
|||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const separator = style({
|
||||
height: "100%",
|
||||
width: "1px",
|
||||
backgroundColor: vars.color.border,
|
||||
margin: `${SPACING_UNIT}px 0`,
|
||||
});
|
||||
|
|
|
@ -9,11 +9,14 @@ import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
|
|||
import { AddDownloadSourceModal } from "./add-download-source-modal";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export function SettingsDownloadSources() {
|
||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||
useState(false);
|
||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
||||
useState(false);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
|
@ -41,6 +44,20 @@ export function SettingsDownloadSources() {
|
|||
showSuccessToast(t("added_download_source"));
|
||||
};
|
||||
|
||||
const syncDownloadSources = async () => {
|
||||
setIsSyncingDownloadSources(true);
|
||||
|
||||
window.electron
|
||||
.syncDownloadSources()
|
||||
.then(() => {
|
||||
showSuccessToast(t("download_sources_synced"));
|
||||
getDownloadSources();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSyncingDownloadSources(false);
|
||||
});
|
||||
};
|
||||
|
||||
const statusTitle = {
|
||||
[DownloadSourceStatus.UpToDate]: t("download_source_up_to_date"),
|
||||
[DownloadSourceStatus.Errored]: t("download_source_errored"),
|
||||
|
@ -62,25 +79,31 @@ export function SettingsDownloadSources() {
|
|||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||
disabled={!downloadSources.length || isSyncingDownloadSources}
|
||||
onClick={syncDownloadSources}
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
{t("add_download_source")}
|
||||
<SyncIcon />
|
||||
{t("sync_download_sources")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
disabled={!downloadSources.length}
|
||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||
>
|
||||
<SyncIcon />
|
||||
{t("resync_download_sources")}
|
||||
<PlusCircleIcon />
|
||||
{t("add_download_source")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul className={styles.downloadSources}>
|
||||
{downloadSources.map((downloadSource) => (
|
||||
<li key={downloadSource.id} className={styles.downloadSourceItem}>
|
||||
<li
|
||||
key={downloadSource.id}
|
||||
className={styles.downloadSourceItem({
|
||||
isSyncing: isSyncingDownloadSources,
|
||||
})}
|
||||
>
|
||||
<div className={styles.downloadSourceItemHeader}>
|
||||
<h2>{downloadSource.name}</h2>
|
||||
|
||||
|
@ -88,6 +111,23 @@ export function SettingsDownloadSources() {
|
|||
<Badge>{statusTitle[downloadSource.status]}</Badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<small>
|
||||
{t("download_count", {
|
||||
count: downloadSource.downloadCount,
|
||||
countFormatted:
|
||||
downloadSource.downloadCount.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
|
||||
<div className={styles.separator} />
|
||||
|
||||
<small>
|
||||
{t("download_options", {
|
||||
count: downloadSource.repackCount,
|
||||
|
@ -95,6 +135,7 @@ export function SettingsDownloadSources() {
|
|||
})}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.downloadSourceField}>
|
||||
<TextField
|
||||
|
|
|
@ -248,6 +248,7 @@ export interface DownloadSource {
|
|||
url: string;
|
||||
repackCount: number;
|
||||
status: DownloadSourceStatus;
|
||||
downloadCount: number;
|
||||
etag: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
Loading…
Reference in a new issue