mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
27 Commits
v3.0.5
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22896303d | ||
|
|
a9085ec2ed | ||
|
|
452532e18b | ||
|
|
b55bc935f7 | ||
|
|
e8fc71b36b | ||
|
|
6295637b48 | ||
|
|
e1a5a95ceb | ||
|
|
e5dff9babe | ||
|
|
5d59a15bbf | ||
|
|
f555890b4c | ||
|
|
d4b1569892 | ||
|
|
e0f7f34d14 | ||
|
|
cd4715e00d | ||
|
|
5daad057e7 | ||
|
|
3a3c0a2b35 | ||
|
|
64b1795ddd | ||
|
|
86d3f7ac81 | ||
|
|
1458314df6 | ||
|
|
1ad501c64e | ||
|
|
92ec056ba8 | ||
|
|
bd8974c7cb | ||
|
|
83b7fb83ab | ||
|
|
dc4dda7e17 | ||
|
|
1ebf8acb9b | ||
|
|
0955af1e69 | ||
|
|
fd7f2403da | ||
|
|
e331b9b246 |
@@ -1,4 +1,4 @@
|
||||
MAIN_VITE_API_URL=API_URL
|
||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID
|
||||
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -44,7 +44,6 @@ jobs:
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Windows
|
||||
@@ -55,7 +54,6 @@ jobs:
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create artifact
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -46,8 +46,8 @@ jobs:
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: yarn build:win
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fontsource/noto-sans": "^5.0.22",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
"queued": "{{title}} (Queued)",
|
||||
"game_has_no_executable": "Game has no executable selected",
|
||||
"sign_in": "Sign in",
|
||||
"friends": "Friends",
|
||||
"need_help": "Need help?"
|
||||
"friends": "Friends"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@@ -199,7 +198,10 @@
|
||||
"queued": "Queued",
|
||||
"no_downloads_title": "Such empty",
|
||||
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
|
||||
"checking_files": "Checking files…"
|
||||
"checking_files": "Checking files…",
|
||||
"seeding": "Seeding",
|
||||
"stop_seed": "Stop seed",
|
||||
"resume_seed": "Resume seed"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
@@ -254,8 +256,9 @@
|
||||
"must_be_valid_url": "The source must be a valid URL",
|
||||
"blocked_users": "Blocked users",
|
||||
"user_unblocked": "User has been unblocked",
|
||||
"enable_achievement_notifications": "When an achievement is unlocked",
|
||||
"launch_minimized": "Launch Hydra minimized"
|
||||
"enable_achievement_notifications": "When an achievement in unlocked",
|
||||
"launch_minimized": "Launch Hydra minimized",
|
||||
"seed_after_download_completes": "Seed after download completes"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
"queued": "{{title}} (En cola)",
|
||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"friends": "Amigos",
|
||||
"need_help": "¿Necesitas ayuda?"
|
||||
"friends": "Amigos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar juegos",
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||
"sign_in": "Login",
|
||||
"friends": "Amigos",
|
||||
"need_help": "Precisa de ajuda?"
|
||||
"friends": "Amigos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
@@ -195,7 +194,10 @@
|
||||
"queued": "Na fila",
|
||||
"no_downloads_title": "Nada por aqui…",
|
||||
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
|
||||
"checking_files": "Verificando arquivos…"
|
||||
"checking_files": "Verificando arquivos…",
|
||||
"seeding": "Semeando",
|
||||
"stop_seed": "Parar seed",
|
||||
"resume_seed": "Retomar seed"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
"queued": "{{title}} (В очереди)",
|
||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||
"sign_in": "Войти",
|
||||
"friends": "Друзья",
|
||||
"need_help": "Нужна помощь?"
|
||||
"friends": "Друзья"
|
||||
},
|
||||
"header": {
|
||||
"search": "Поиск",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
SeedList,
|
||||
} from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
@@ -25,6 +26,7 @@ export const dataSource = new DataSource({
|
||||
DownloadSource,
|
||||
DownloadQueue,
|
||||
GameAchievement,
|
||||
SeedList,
|
||||
],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
|
||||
@@ -54,6 +54,9 @@ export class Game {
|
||||
@Column("int", { default: Downloader.Torrent })
|
||||
downloader: Downloader;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
shouldSeed: boolean;
|
||||
|
||||
/**
|
||||
* Progress is a float between 0 and 1
|
||||
*/
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./game.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
export * from "./seed-list.entity";
|
||||
|
||||
25
src/main/entity/seed-list.entity.ts
Normal file
25
src/main/entity/seed-list.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity("seed_list")
|
||||
export class SeedList {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text")
|
||||
downloadUri: string;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
shouldSeed: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -38,6 +38,9 @@ export class UserPreferences {
|
||||
@Column("boolean", { default: false })
|
||||
startMinimized: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
seedAfterDownloadCompletes: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@@ -91,15 +91,9 @@ const startGameDownload = async (
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
if (uri.startsWith("magnet:")) {
|
||||
try {
|
||||
const { infoHash } = await parseTorrent(payload.uri);
|
||||
if (infoHash) {
|
||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to parse torrent", err);
|
||||
}
|
||||
const { infoHash } = await parseTorrent(payload.uri);
|
||||
if (infoHash) {
|
||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
||||
}
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
|
||||
@@ -12,6 +12,9 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
|
||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
||||
import { AddSeedAfterDownloadCompletesColumn } from "./migrations/20241101012727_add_seed_after_download_completes_column";
|
||||
import { AddSeedListTable } from "./migrations/20241103231555_add_seed_list_table";
|
||||
import { AddShouldSeedColumn } from "./migrations/20241107211345_add_should_seed_colum";
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
@@ -28,6 +31,9 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
AddBackgroundImageUrl,
|
||||
AddWinePrefixToGame,
|
||||
AddStartMinimizedColumn,
|
||||
AddSeedAfterDownloadCompletesColumn,
|
||||
AddSeedListTable,
|
||||
AddShouldSeedColumn,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddSeedAfterDownloadCompletesColumn: HydraMigration = {
|
||||
name: "AddSeedAfterDownloadCompletesColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table
|
||||
.boolean("seedAfterDownloadCompletes")
|
||||
.notNullable()
|
||||
.defaultTo(1);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("seedAfterDownloadCompletes");
|
||||
});
|
||||
},
|
||||
};
|
||||
19
src/main/migrations/20241103231555_add_seed_list_table.ts
Normal file
19
src/main/migrations/20241103231555_add_seed_list_table.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddSeedListTable: HydraMigration = {
|
||||
name: "AddSeedListTable",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.createTable("seed_list", async (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("downloadUri").notNullable();
|
||||
table.boolean("shouldSeed").defaultTo(false);
|
||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.dropTable("seed_list");
|
||||
},
|
||||
};
|
||||
17
src/main/migrations/20241107211345_add_should_seed_colum.ts
Normal file
17
src/main/migrations/20241107211345_add_should_seed_colum.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddShouldSeedColumn: HydraMigration = {
|
||||
name: "AddShouldSeedColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.boolean("shouldSeed").notNullable().defaultTo(false);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.dropColumn("shouldSeed");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
SeedList,
|
||||
} from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
@@ -27,6 +28,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const seedListRepository = dataSource.getRepository(SeedList);
|
||||
|
||||
export const userSubscriptionRepository =
|
||||
dataSource.getRepository(UserSubscription);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export const mergeAchievements = async (
|
||||
);
|
||||
});
|
||||
})
|
||||
.filter((achievement) => Boolean(achievement))
|
||||
.filter((achievement) => achievement)
|
||||
.map((achievement) => {
|
||||
return {
|
||||
displayName: achievement!.displayName,
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Game } from "@main/entity";
|
||||
import { Downloader } from "@shared";
|
||||
import { PythonInstance } from "./python-instance";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||
import type { DownloadProgress } from "@types";
|
||||
@@ -50,6 +54,19 @@ export class DownloadManager {
|
||||
|
||||
await downloadQueueRepository.delete({ game });
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (
|
||||
userPreferences?.seedAfterDownloadCompletes &&
|
||||
this.currentDownloader === Downloader.Torrent
|
||||
) {
|
||||
if (!game.shouldSeed) {
|
||||
await gameRepository.update(game.id, { shouldSeed: true });
|
||||
}
|
||||
}
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
@@ -66,6 +83,21 @@ export class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static async watchSeedingList() {
|
||||
const shouldSeedGames = await gameRepository.findOne({
|
||||
where: { shouldSeed: true },
|
||||
});
|
||||
|
||||
if (shouldSeedGames) {
|
||||
const seedingList = await PythonInstance.getSeedingList();
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-seeding-list",
|
||||
JSON.parse(JSON.stringify(seedingList))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async pauseDownload() {
|
||||
if (this.currentDownloader === Downloader.Torrent) {
|
||||
await PythonInstance.pauseDownload();
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
StartDownloadPayload,
|
||||
PauseDownloadPayload,
|
||||
LibtorrentStatus,
|
||||
LibtorrentPayload,
|
||||
ProcessPayload,
|
||||
LibtorrentSeedingPayload,
|
||||
LibtorrentDownloadingPayload,
|
||||
} from "./types";
|
||||
import { pythonInstanceLogger as logger } from "../logger";
|
||||
|
||||
@@ -60,67 +61,87 @@ export class PythonInstance {
|
||||
);
|
||||
}
|
||||
|
||||
public static async getSeedingList() {
|
||||
const response = await this.rpc.get<LibtorrentSeedingPayload[] | null>(
|
||||
"/seed-list"
|
||||
);
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
for (const seed of response.data) {
|
||||
await gameRepository.update({ id: seed.gameId }, { status: "seeding" });
|
||||
}
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public static async getStatus() {
|
||||
if (this.downloadingGameId === -1) return null;
|
||||
const response = await this.rpc.get<LibtorrentDownloadingPayload | null>(
|
||||
"/status"
|
||||
);
|
||||
|
||||
const response = await this.rpc.get<LibtorrentPayload | null>("/status");
|
||||
|
||||
if (response.data === null) return null;
|
||||
|
||||
try {
|
||||
const {
|
||||
progress,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
downloadSpeed,
|
||||
bytesDownloaded,
|
||||
fileSize,
|
||||
folderName,
|
||||
status,
|
||||
gameId,
|
||||
} = response.data;
|
||||
|
||||
this.downloadingGameId = gameId;
|
||||
|
||||
const isDownloadingMetadata =
|
||||
status === LibtorrentStatus.DownloadingMetadata;
|
||||
|
||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||
|
||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
if (response.data) {
|
||||
try {
|
||||
const {
|
||||
progress,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
downloadSpeed,
|
||||
bytesDownloaded,
|
||||
fileSize,
|
||||
folderName,
|
||||
status,
|
||||
gameId,
|
||||
} = response.data;
|
||||
|
||||
this.downloadingGameId = gameId;
|
||||
|
||||
const isDownloadingMetadata =
|
||||
status === LibtorrentStatus.DownloadingMetadata;
|
||||
|
||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||
|
||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded,
|
||||
fileSize,
|
||||
progress,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{
|
||||
...update,
|
||||
folderName,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
progress === 1 &&
|
||||
!isCheckingFiles &&
|
||||
status !== LibtorrentStatus.Seeding
|
||||
) {
|
||||
this.downloadingGameId = -1;
|
||||
}
|
||||
|
||||
return {
|
||||
numPeers,
|
||||
numSeeds,
|
||||
downloadSpeed,
|
||||
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
||||
isDownloadingMetadata,
|
||||
isCheckingFiles,
|
||||
progress,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{
|
||||
...update,
|
||||
folderName,
|
||||
}
|
||||
);
|
||||
gameId,
|
||||
} as DownloadProgress;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (progress === 1 && !isCheckingFiles) {
|
||||
this.downloadingGameId = -1;
|
||||
}
|
||||
|
||||
return {
|
||||
numPeers,
|
||||
numSeeds,
|
||||
downloadSpeed,
|
||||
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
||||
isDownloadingMetadata,
|
||||
isCheckingFiles,
|
||||
progress,
|
||||
gameId,
|
||||
} as DownloadProgress;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async pauseDownload() {
|
||||
|
||||
@@ -20,7 +20,7 @@ export enum LibtorrentStatus {
|
||||
Seeding = 5,
|
||||
}
|
||||
|
||||
export interface LibtorrentPayload {
|
||||
export interface LibtorrentDownloadingPayload {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
@@ -32,7 +32,22 @@ export interface LibtorrentPayload {
|
||||
gameId: number;
|
||||
}
|
||||
|
||||
export interface LibtorrentSeedingPayload {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
uploadSpeed: number;
|
||||
fileSize: number;
|
||||
folderName: string;
|
||||
status: LibtorrentStatus;
|
||||
gameId: number;
|
||||
}
|
||||
|
||||
export interface ProcessPayload {
|
||||
exe: string;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
export interface SeedPayload {
|
||||
should_seed: boolean;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const startMainLoop = async () => {
|
||||
watchProcesses(),
|
||||
DownloadManager.watchDownloads(),
|
||||
AchievementWatcherManager.watchAchievements(),
|
||||
DownloadManager.watchSeedingList(),
|
||||
]);
|
||||
|
||||
await sleep(1500);
|
||||
|
||||
@@ -56,7 +56,6 @@ export const getUserData = () => {
|
||||
id: loggedUser.userId,
|
||||
username: "",
|
||||
bio: "",
|
||||
email: null,
|
||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||
subscription: loggedUser.subscription
|
||||
? {
|
||||
|
||||
@@ -85,10 +85,6 @@ export class WindowManager {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("intercom.io")) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"access-control-allow-origin": ["*"],
|
||||
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import { GameAchievement } from "@main/entity";
|
||||
import { LibtorrentSeedingPayload } from "@main/services/download/types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
/* Torrenting */
|
||||
@@ -26,6 +27,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("pauseGameDownload", gameId),
|
||||
resumeGameDownload: (gameId: number) =>
|
||||
ipcRenderer.invoke("resumeGameDownload", gameId),
|
||||
startSeeding: (gameId: number, magnet: string, savePath: string) =>
|
||||
ipcRenderer.invoke("startSeeding", gameId, magnet, savePath),
|
||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
@@ -34,6 +37,14 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.on("on-download-progress", listener);
|
||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||
},
|
||||
onSeedingList: (cb: (value: LibtorrentSeedingPayload[]) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
value: LibtorrentSeedingPayload[]
|
||||
) => cb(value);
|
||||
ipcRenderer.on("on-seeding-list", listener);
|
||||
return () => ipcRenderer.removeListener("on-seeding-list", listener);
|
||||
},
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Hydra</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -126,9 +126,3 @@ export const titleBar = style({
|
||||
zIndex: "4",
|
||||
borderBottom: `1px solid ${vars.color.border}`,
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const cloudText = style({
|
||||
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useCallback, useContext, useEffect, useRef } from "react";
|
||||
|
||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||
|
||||
import Intercom from "@intercom/messenger-js-sdk";
|
||||
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
@@ -36,12 +34,6 @@ export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
console.log(import.meta.env);
|
||||
|
||||
Intercom({
|
||||
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
|
||||
});
|
||||
|
||||
export function App() {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const { updateLibrary, library } = useLibrary();
|
||||
@@ -62,13 +54,8 @@ export function App() {
|
||||
hideFriendsModal,
|
||||
} = useUserDetails();
|
||||
|
||||
const {
|
||||
userDetails,
|
||||
hasActiveSubscription,
|
||||
fetchUserDetails,
|
||||
updateUserDetails,
|
||||
clearUserDetails,
|
||||
} = useUserDetails();
|
||||
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||
useUserDetails();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -217,9 +204,7 @@ export function App() {
|
||||
|
||||
useEffect(() => {
|
||||
new MutationObserver(() => {
|
||||
const modal = document.body.querySelector(
|
||||
"[role=dialog]:not([data-intercom-frame='true'])"
|
||||
);
|
||||
const modal = document.body.querySelector("[role=dialog]");
|
||||
|
||||
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
||||
}).observe(document.body, {
|
||||
@@ -285,12 +270,7 @@ export function App() {
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
<div className={styles.titleBar}>
|
||||
<h4>
|
||||
Hydra
|
||||
{hasActiveSubscription && (
|
||||
<span className={styles.cloudText}> Cloud</span>
|
||||
)}
|
||||
</h4>
|
||||
<h4>Hydra</h4>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
color: globals.$muted-color;
|
||||
font-size: 10px;
|
||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
||||
border: solid 1px globals.$muted-color;
|
||||
border: solid 1px globals.$border-color;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -13,7 +13,6 @@ export const sidebar = recipe({
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
variants: {
|
||||
resizing: {
|
||||
@@ -125,28 +124,3 @@ export const section = style({
|
||||
flexDirection: "column",
|
||||
paddingBottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const helpButton = style({
|
||||
color: vars.color.muted,
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||
gap: "9px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
borderTop: `solid 1px ${vars.color.border}`,
|
||||
transition: "background-color ease 0.1s",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const helpButtonIcon = style({
|
||||
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#fff",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
@@ -5,12 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
import { TextField } from "@renderer/components";
|
||||
import {
|
||||
useDownload,
|
||||
useLibrary,
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||
|
||||
import { routes } from "./routes";
|
||||
|
||||
@@ -20,9 +15,6 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||
|
||||
import { show, update } from "@intercom/messenger-js-sdk";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||
@@ -50,20 +42,6 @@ export function Sidebar() {
|
||||
return sortBy(library, (game) => game.title);
|
||||
}, [library]);
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails) {
|
||||
update({
|
||||
name: userDetails.displayName,
|
||||
Username: userDetails.username,
|
||||
Email: userDetails.email,
|
||||
"Subscription expiration date": userDetails?.subscription?.expiresAt,
|
||||
"Payment status": userDetails?.subscription?.status,
|
||||
});
|
||||
}
|
||||
}, [userDetails, hasActiveSubscription]);
|
||||
|
||||
const { lastPacket, progress } = useDownload();
|
||||
|
||||
const { showWarningToast } = useToast();
|
||||
@@ -188,91 +166,77 @@ export function Sidebar() {
|
||||
maxWidth: sidebarWidth,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
|
||||
>
|
||||
<SidebarProfile />
|
||||
<SidebarProfile />
|
||||
|
||||
<div className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
<li
|
||||
key={nameKey}
|
||||
className={styles.menuItem({
|
||||
active: location.pathname === path,
|
||||
})}
|
||||
<div className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
<li
|
||||
key={nameKey}
|
||||
className={styles.menuItem({
|
||||
active: location.pathname === path,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
{render()}
|
||||
<span>{t(nameKey)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{render()}
|
||||
<span>{t(nameKey)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
|
||||
<TextField
|
||||
ref={filterRef}
|
||||
placeholder={t("filter")}
|
||||
onChange={handleFilter}
|
||||
theme="dark"
|
||||
/>
|
||||
<TextField
|
||||
ref={filterRef}
|
||||
placeholder={t("filter")}
|
||||
onChange={handleFilter}
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname ===
|
||||
`/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{hasActiveSubscription && (
|
||||
<button type="button" className={styles.helpButton} onClick={show}>
|
||||
<div className={styles.helpButtonIcon}>
|
||||
<CommentDiscussionIcon size={14} />
|
||||
</div>
|
||||
<span>{t("need_help")}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.handle}
|
||||
|
||||
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@@ -49,6 +49,9 @@ declare global {
|
||||
onDownloadProgress: (
|
||||
cb: (value: DownloadProgress) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onSeedingList: (
|
||||
cb: (value: LibtorrentSeedingPayload[]) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
|
||||
@@ -10,22 +10,12 @@ export interface HowLongToBeatEntry {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CatalogueCache {
|
||||
id?: number;
|
||||
category: string;
|
||||
games: { objectId: string; shop: GameShop }[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export const db = new Dexie("Hydra");
|
||||
|
||||
db.version(5).stores({
|
||||
db.version(4).stores({
|
||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
||||
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
|
||||
});
|
||||
|
||||
export const downloadSourcesTable = db.table("downloadSources");
|
||||
@@ -34,6 +24,4 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
|
||||
"howLongToBeatEntries"
|
||||
);
|
||||
|
||||
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
|
||||
|
||||
db.open();
|
||||
|
||||
0
src/renderer/src/hooks/use-friendship.ts
Normal file
0
src/renderer/src/hooks/use-friendship.ts
Normal file
@@ -1,6 +1,7 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
import type { LibraryGame, SeedingList } from "@types";
|
||||
|
||||
import { Badge, Button } from "@renderer/components";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ export interface DownloadGroupProps {
|
||||
title: string;
|
||||
openDeleteGameModal: (gameId: number) => void;
|
||||
openGameInstaller: (gameId: number) => void;
|
||||
seedingList: SeedingList[];
|
||||
}
|
||||
|
||||
export function DownloadGroup({
|
||||
@@ -28,6 +30,7 @@ export function DownloadGroup({
|
||||
title,
|
||||
openDeleteGameModal,
|
||||
openGameInstaller,
|
||||
seedingList = [],
|
||||
}: DownloadGroupProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -46,6 +49,17 @@ export function DownloadGroup({
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const seedingMap = useMemo(() => {
|
||||
if (!Array.isArray(seedingList) || seedingList.length === 0) {
|
||||
return new Map<number, SeedingList>();
|
||||
}
|
||||
const map = new Map<number, SeedingList>();
|
||||
seedingList.forEach((seed) => {
|
||||
map.set(seed.gameId, seed);
|
||||
});
|
||||
return map;
|
||||
}, [seedingList]);
|
||||
|
||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
@@ -60,6 +74,7 @@ export function DownloadGroup({
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
const seed = seedingMap.get(game.id);
|
||||
|
||||
if (isGameDeleting(game.id)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
@@ -98,7 +113,18 @@ export function DownloadGroup({
|
||||
}
|
||||
|
||||
if (game.progress === 1) {
|
||||
return <p>{t("completed")}</p>;
|
||||
return (
|
||||
<>
|
||||
{seed ? (
|
||||
<>
|
||||
<p>{t("seeding")}</p>
|
||||
<p>{formatBytes(seed.uploadSpeed ?? 0)}/s</p>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("completed")}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game.status === "paused") {
|
||||
@@ -127,8 +153,8 @@ export function DownloadGroup({
|
||||
|
||||
const getGameActions = (game: LibraryGame) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
const seed = seedingMap.get(game.id);
|
||||
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
@@ -144,6 +170,14 @@ export function DownloadGroup({
|
||||
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
|
||||
{t("delete")}
|
||||
</Button>
|
||||
|
||||
{seed && game.shouldSeed && (
|
||||
<Button theme="outline">{t("stop_seed")}</Button>
|
||||
)}
|
||||
|
||||
{seed && !game.shouldSeed && (
|
||||
<Button theme="outline">{t("resume_seed")}</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { LibraryGame } from "@types";
|
||||
import type { LibraryGame, SeedingList } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
@@ -30,6 +30,12 @@ export default function Downloads() {
|
||||
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const [seedingList, setSeedingList] = useState<SeedingList[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.onSeedingList((value) => setSeedingList(value));
|
||||
}, []);
|
||||
|
||||
const handleOpenGameInstaller = (gameId: number) =>
|
||||
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||
@@ -122,6 +128,7 @@ export default function Downloads() {
|
||||
library={group.library}
|
||||
openDeleteGameModal={handleOpenDeleteGameModal}
|
||||
openGameInstaller={handleOpenGameInstaller}
|
||||
seedingList={seedingList}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,14 +15,6 @@ import * as styles from "./home.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { CatalogueCategory } from "@shared";
|
||||
import { catalogueCacheTable, db } from "@renderer/dexie";
|
||||
import { add } from "date-fns";
|
||||
|
||||
const categoryCacheDurationInSeconds = {
|
||||
[CatalogueCategory.Hot]: 60 * 60 * 2,
|
||||
[CatalogueCategory.Weekly]: 60 * 60 * 24,
|
||||
[CatalogueCategory.Achievements]: 60 * 60 * 24,
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useTranslation("home");
|
||||
@@ -44,43 +36,19 @@ export default function Home() {
|
||||
[CatalogueCategory.Achievements]: [],
|
||||
});
|
||||
|
||||
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
|
||||
try {
|
||||
const catalogueCache = await catalogueCacheTable
|
||||
.where("expiresAt")
|
||||
.above(new Date())
|
||||
.and((cache) => cache.category === category)
|
||||
.first();
|
||||
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
||||
setCurrentCatalogueCategory(category);
|
||||
setIsLoading(true);
|
||||
|
||||
setCurrentCatalogueCategory(category);
|
||||
setIsLoading(true);
|
||||
|
||||
if (catalogueCache)
|
||||
return setCatalogue((prev) => ({
|
||||
...prev,
|
||||
[category]: catalogueCache.games,
|
||||
}));
|
||||
|
||||
const catalogue = await window.electron.getCatalogue(category);
|
||||
|
||||
db.transaction("rw", catalogueCacheTable, async () => {
|
||||
await catalogueCacheTable.where("category").equals(category).delete();
|
||||
|
||||
await catalogueCacheTable.add({
|
||||
category,
|
||||
games: catalogue,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
expiresAt: add(new Date(), {
|
||||
seconds: categoryCacheDurationInSeconds[category],
|
||||
}),
|
||||
});
|
||||
window.electron
|
||||
.getCatalogue(category)
|
||||
.then((catalogue) => {
|
||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getRandomGame = useCallback(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export function SettingsBehavior() {
|
||||
preferQuitInsteadOfHiding: false,
|
||||
runAtStartup: false,
|
||||
startMinimized: false,
|
||||
seedAfterDownloadCompletes: true,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
@@ -28,6 +29,7 @@ export function SettingsBehavior() {
|
||||
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
|
||||
runAtStartup: userPreferences.runAtStartup,
|
||||
startMinimized: userPreferences.startMinimized,
|
||||
seedAfterDownloadCompletes: userPreferences.seedAfterDownloadCompletes,
|
||||
});
|
||||
}
|
||||
}, [userPreferences]);
|
||||
@@ -86,6 +88,16 @@ export function SettingsBehavior() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CheckboxField
|
||||
label={t("seed_after_download_completes")}
|
||||
checked={form.seedAfterDownloadCompletes}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
seedAfterDownloadCompletes: !form.seedAfterDownloadCompletes,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
8
src/renderer/src/vite-env.d.ts
vendored
8
src/renderer/src/vite-env.d.ts
vendored
@@ -1,10 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export type GameStatus =
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "removed";
|
||||
| "removed"
|
||||
| "seeding";
|
||||
|
||||
export type GameShop = "steam" | "epic";
|
||||
|
||||
@@ -124,6 +125,7 @@ export interface Game {
|
||||
objectID: string;
|
||||
shop: GameShop;
|
||||
downloadQueue: DownloadQueue | null;
|
||||
shouldSeed: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -151,6 +153,16 @@ export interface DownloadProgress {
|
||||
game: LibraryGame;
|
||||
}
|
||||
|
||||
export interface SeedingList {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
uploadSpeed: number;
|
||||
gameId: number;
|
||||
folderName: string;
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
downloadsPath: string | null;
|
||||
language: string;
|
||||
@@ -161,6 +173,7 @@ export interface UserPreferences {
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
runAtStartup: boolean;
|
||||
startMinimized: boolean;
|
||||
seedAfterDownloadCompletes: boolean;
|
||||
}
|
||||
|
||||
export interface Steam250Game {
|
||||
@@ -245,7 +258,6 @@ export interface Subscription {
|
||||
export interface UserDetails {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
backgroundImageUrl: string | null;
|
||||
@@ -258,7 +270,6 @@ export interface UserProfile {
|
||||
id: string;
|
||||
displayName: string;
|
||||
profileImageUrl: string | null;
|
||||
email: string | null;
|
||||
backgroundImageUrl: string | null;
|
||||
profileVisibility: ProfileVisibility;
|
||||
libraryGames: UserGame[];
|
||||
@@ -375,4 +386,4 @@ export interface ComparedAchievements {
|
||||
export * from "./steam.types";
|
||||
export * from "./real-debrid.types";
|
||||
export * from "./ludusavi.types";
|
||||
export * from "./how-long-to-beat.types";
|
||||
export * from "./howlongtobeat.types";
|
||||
|
||||
@@ -50,6 +50,21 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||
|
||||
|
||||
elif self.path == "/seed-list":
|
||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||
self.send_response(401)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
seed_list = torrent_downloader.get_seed_list()
|
||||
|
||||
self.wfile.write(json.dumps(seed_list).encode('utf-8'))
|
||||
|
||||
elif self.path == "/healthcheck":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
@@ -107,6 +122,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif data['action'] == 'kill-torrent':
|
||||
torrent_downloader.abort_session()
|
||||
torrent_downloader = None
|
||||
elif data['action'] == 'start-seeding':
|
||||
torrent_downloader.start_seeding(data['game_id'], data['magnet'], data['save_path'])
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
@@ -106,7 +106,7 @@ class TorrentDownloader:
|
||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
||||
torrent_handle = self.session.add_torrent(params)
|
||||
self.torrent_handles[game_id] = torrent_handle
|
||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed, lt.torrent_flags.seed_mode)
|
||||
torrent_handle.resume()
|
||||
|
||||
self.downloading_game_id = game_id
|
||||
@@ -151,6 +151,7 @@ class TorrentDownloader:
|
||||
'gameId': self.downloading_game_id,
|
||||
'progress': status.progress,
|
||||
'downloadSpeed': status.download_rate,
|
||||
'uploadSpeed': status.upload_rate,
|
||||
'numPeers': status.num_peers,
|
||||
'numSeeds': status.num_seeds,
|
||||
'status': status.state,
|
||||
@@ -158,8 +159,34 @@ class TorrentDownloader:
|
||||
}
|
||||
|
||||
if status.progress == 1:
|
||||
torrent_handle.pause()
|
||||
self.session.remove_torrent(torrent_handle)
|
||||
self.downloading_game_id = -1
|
||||
|
||||
return response
|
||||
|
||||
def get_seed_list(self):
|
||||
response = []
|
||||
|
||||
for game_id, torrent_handle in self.torrent_handles.items():
|
||||
if game_id == self.downloading_game_id:
|
||||
continue
|
||||
|
||||
status = torrent_handle.status()
|
||||
info = torrent_handle.torrent_file()
|
||||
|
||||
torrent_info = {
|
||||
'folderName': info.name() if info else "",
|
||||
'fileSize': info.total_size() if info else 0,
|
||||
'gameId': game_id,
|
||||
'progress': status.progress,
|
||||
'downloadSpeed': status.download_rate,
|
||||
'uploadSpeed': status.upload_rate,
|
||||
'numPeers': status.num_peers,
|
||||
'numSeeds': status.num_seeds,
|
||||
'status': status.state,
|
||||
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
||||
}
|
||||
|
||||
if status.state == 5:
|
||||
response.append(torrent_info)
|
||||
|
||||
return response
|
||||
@@ -1066,11 +1066,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
|
||||
"@intercom/messenger-js-sdk@^0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.14.tgz#a27999370cc0a82a2a57a779426df25a57891863"
|
||||
integrity sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user