feat: moving notifications

This commit is contained in:
Chubby Granny Chaser 2024-06-05 14:18:40 +01:00
parent d6e57c20c7
commit ef036d6f57
No known key found for this signature in database
17 changed files with 179 additions and 87 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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,

View file

@ -0,0 +1,7 @@
import { registerEvent } from "../register-event";
import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
fetchDownloadSourcesAndUpdate();
registerEvent("syncDownloadSources", syncDownloadSources);

View file

@ -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());

View file

@ -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();
});

View file

@ -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 });

View 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();
}
};

View file

@ -38,6 +38,7 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
results.push({
...downloadSource,
downloads: [],
etag: null,
status: isNotModified
? DownloadSourceStatus.UpToDate
: DownloadSourceStatus.Errored,

View file

@ -58,6 +58,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("addDownloadSource", url),
removeDownloadSource: (id: number) =>
ipcRenderer.invoke("removeDownloadSource", id),
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
/* Library */
addGameToLibrary: (

View file

@ -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 })}>

View file

@ -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>;

View file

@ -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);

View file

@ -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`,
});

View file

@ -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

View file

@ -248,6 +248,7 @@ export interface DownloadSource {
url: string;
repackCount: number;
status: DownloadSourceStatus;
downloadCount: number;
etag: string | null;
createdAt: Date;
updatedAt: Date;