mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge branch 'main' into main
This commit is contained in:
commit
c6e313f5ad
@ -1,3 +1,4 @@
|
|||||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||||
MAIN_VITE_API_URL=API_URL
|
MAIN_VITE_API_URL=API_URL
|
||||||
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -37,8 +37,6 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: yarn build:linux
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
|
|
||||||
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
|
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
@ -48,8 +46,6 @@ jobs:
|
|||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
|
|
||||||
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
|
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -39,8 +39,6 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: yarn build:linux
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
|
|
||||||
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
|
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
@ -50,8 +48,6 @@ jobs:
|
|||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
env:
|
env:
|
||||||
MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
|
|
||||||
MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
|
|
||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
|
@ -5,3 +5,4 @@ pnpm-lock.yaml
|
|||||||
LICENSE.md
|
LICENSE.md
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
tsconfig.*.json
|
tsconfig.*.json
|
||||||
|
src/main/migrations
|
||||||
|
@ -174,12 +174,9 @@
|
|||||||
"validate_download_source": "Validate",
|
"validate_download_source": "Validate",
|
||||||
"remove_download_source": "Remove",
|
"remove_download_source": "Remove",
|
||||||
"add_download_source": "Add source",
|
"add_download_source": "Add source",
|
||||||
"download_count_zero": "No downloads in list",
|
"download_count_zero": "No download options",
|
||||||
"download_count_one": "{{countFormatted}} download in list",
|
"download_count_one": "{{countFormatted}} download option",
|
||||||
"download_count_other": "{{countFormatted}} downloads in list",
|
"download_count_other": "{{countFormatted}} download options",
|
||||||
"download_options_zero": "No download available",
|
|
||||||
"download_options_one": "{{countFormatted}} download available",
|
|
||||||
"download_options_other": "{{countFormatted}} downloads available",
|
|
||||||
"download_source_url": "Download source URL",
|
"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_up_to_date": "Up-to-date",
|
||||||
@ -261,6 +258,18 @@
|
|||||||
"undo_friendship": "Undo friendship",
|
"undo_friendship": "Undo friendship",
|
||||||
"request_accepted": "Request accepted",
|
"request_accepted": "Request accepted",
|
||||||
"user_blocked_successfully": "User blocked successfully",
|
"user_blocked_successfully": "User blocked successfully",
|
||||||
"user_block_modal_text": "This will block {{displayName}}"
|
"user_block_modal_text": "This will block {{displayName}}",
|
||||||
|
"settings": "Settings",
|
||||||
|
"public": "Public",
|
||||||
|
"private": "Private",
|
||||||
|
"friends_only": "Friends only",
|
||||||
|
"privacy": "Privacy",
|
||||||
|
"blocked_users": "Blocked users",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"no_friends_added": "You still don't have added friends",
|
||||||
|
"pending": "Pending",
|
||||||
|
"no_pending_invites": "You have no pending invites",
|
||||||
|
"no_blocked_users": "You have no blocked users",
|
||||||
|
"friend_code_copied": "Friend code copied"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,6 +261,18 @@
|
|||||||
"undo_friendship": "Desfazer amizade",
|
"undo_friendship": "Desfazer amizade",
|
||||||
"request_accepted": "Pedido de amizade aceito",
|
"request_accepted": "Pedido de amizade aceito",
|
||||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
||||||
"user_block_modal_text": "Bloquear {{displayName}}"
|
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||||
|
"settings": "Configurações",
|
||||||
|
"privacy": "Privacidade",
|
||||||
|
"private": "Privado",
|
||||||
|
"friends_only": "Apenas amigos",
|
||||||
|
"public": "Público",
|
||||||
|
"blocked_users": "Usuários bloqueados",
|
||||||
|
"unblock": "Desbloquear",
|
||||||
|
"no_friends_added": "Você ainda não possui amigos adicionados",
|
||||||
|
"pending": "Pendentes",
|
||||||
|
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
||||||
|
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
||||||
|
"friend_code_copied": "Código de amigo copiado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,32 +6,24 @@ import {
|
|||||||
GameShopCache,
|
GameShopCache,
|
||||||
Repack,
|
Repack,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
UserAuth,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
import migrations from "./migrations";
|
import * as migrations from "./migrations";
|
||||||
import { UserAuth } from "./entity/user-auth";
|
|
||||||
|
|
||||||
export const createDataSource = (
|
export const dataSource = new DataSource({
|
||||||
options: Partial<BetterSqlite3ConnectionOptions>
|
type: "better-sqlite3",
|
||||||
) =>
|
entities: [
|
||||||
new DataSource({
|
Game,
|
||||||
type: "better-sqlite3",
|
Repack,
|
||||||
entities: [
|
UserPreferences,
|
||||||
Game,
|
GameShopCache,
|
||||||
Repack,
|
DownloadSource,
|
||||||
UserPreferences,
|
DownloadQueue,
|
||||||
GameShopCache,
|
UserAuth,
|
||||||
DownloadSource,
|
],
|
||||||
DownloadQueue,
|
synchronize: true,
|
||||||
UserAuth,
|
database: databasePath,
|
||||||
],
|
|
||||||
synchronize: true,
|
|
||||||
database: databasePath,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dataSource = createDataSource({
|
|
||||||
migrations,
|
migrations,
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,14 @@ export class Repack {
|
|||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use uris instead
|
||||||
|
*/
|
||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated Direct scraping capability has been removed
|
||||||
*/
|
*/
|
||||||
@Column("int", { nullable: true })
|
@Column("int", { nullable: true })
|
||||||
page: number;
|
page: number;
|
||||||
@ -37,6 +40,9 @@ export class Repack {
|
|||||||
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
||||||
downloadSource: DownloadSource;
|
downloadSource: DownloadSource;
|
||||||
|
|
||||||
|
@Column("text", { default: "[]" })
|
||||||
|
uris: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { downloadSourceRepository } from "@main/repository";
|
import { downloadSourceRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
return downloadSourceRepository
|
downloadSourceRepository.find({
|
||||||
.createQueryBuilder("downloadSource")
|
order: {
|
||||||
.leftJoin("downloadSource.repacks", "repacks")
|
createdAt: "DESC",
|
||||||
.orderBy("downloadSource.createdAt", "DESC")
|
},
|
||||||
.loadRelationCountAndMap(
|
});
|
||||||
"downloadSource.repackCount",
|
|
||||||
"downloadSource.repacks"
|
|
||||||
)
|
|
||||||
.getMany();
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("getDownloadSources", getDownloadSources);
|
registerEvent("getDownloadSources", getDownloadSources);
|
||||||
|
@ -43,6 +43,7 @@ import "./auth/sign-out";
|
|||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
|
import "./user/get-user-blocks";
|
||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
@ -52,11 +53,9 @@ import "./profile/undo-friendship";
|
|||||||
import "./profile/update-friend-request";
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
ipcMain.handle(
|
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||||
"isPortableVersion",
|
|
||||||
() => process.env.PORTABLE_EXECUTABLE_FILE != null
|
|
||||||
);
|
|
||||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||||
|
@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => {
|
|||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||||
|
|
||||||
if (game?.remoteId) {
|
if (game?.remoteId) {
|
||||||
HydraApi.delete(`/games/${game.remoteId}`).catch(() => {});
|
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,33 +4,22 @@ import axios from "axios";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
import { UserProfile } from "@types";
|
import { UpdateProfileProps, UserProfile } from "@types";
|
||||||
|
|
||||||
const patchUserProfile = async (
|
const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
|
||||||
displayName: string,
|
return HydraApi.patch("/profile", updateProfile);
|
||||||
profileImageUrl?: string
|
|
||||||
) => {
|
|
||||||
if (profileImageUrl) {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
profileImageUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return HydraApi.patch("/profile", {
|
|
||||||
displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async (
|
const updateProfile = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
displayName: string,
|
updateProfile: UpdateProfileProps
|
||||||
newProfileImagePath: string | null
|
|
||||||
): Promise<UserProfile> => {
|
): Promise<UserProfile> => {
|
||||||
if (!newProfileImagePath) {
|
if (!updateProfile.profileImageUrl) {
|
||||||
return patchUserProfile(displayName);
|
return patchUserProfile(updateProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newProfileImagePath = updateProfile.profileImageUrl;
|
||||||
|
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||||
const fileSizeInBytes = stats.size;
|
const fileSizeInBytes = stats.size;
|
||||||
@ -53,7 +42,7 @@ const updateProfile = async (
|
|||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
return patchUserProfile(displayName, profileImageUrl);
|
return patchUserProfile({ ...updateProfile, profileImageUrl });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
@ -18,7 +18,8 @@ const startGameDownload = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
payload: StartGameDownloadPayload
|
payload: StartGameDownloadPayload
|
||||||
) => {
|
) => {
|
||||||
const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
|
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
||||||
|
payload;
|
||||||
|
|
||||||
const [game, repack] = await Promise.all([
|
const [game, repack] = await Promise.all([
|
||||||
gameRepository.findOne({
|
gameRepository.findOne({
|
||||||
@ -54,7 +55,7 @@ const startGameDownload = async (
|
|||||||
bytesDownloaded: 0,
|
bytesDownloaded: 0,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
downloader,
|
downloader,
|
||||||
uri: repack.magnet,
|
uri,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -76,7 +77,7 @@ const startGameDownload = async (
|
|||||||
shop,
|
shop,
|
||||||
status: "active",
|
status: "active",
|
||||||
downloadPath,
|
downloadPath,
|
||||||
uri: repack.magnet,
|
uri,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (iconUrl) {
|
if (iconUrl) {
|
||||||
@ -100,6 +101,7 @@ const startGameDownload = async (
|
|||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||||
|
|
||||||
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ const blockUser = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
await HydraApi.post(`/user/${userId}/block`);
|
await HydraApi.post(`/users/${userId}/block`);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("blockUser", blockUser);
|
registerEvent("blockUser", blockUser);
|
||||||
|
13
src/main/events/user/get-user-blocks.ts
Normal file
13
src/main/events/user/get-user-blocks.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { UserBlocks } from "@types";
|
||||||
|
|
||||||
|
export const getUserBlocks = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
take: number,
|
||||||
|
skip: number
|
||||||
|
): Promise<UserBlocks> => {
|
||||||
|
return HydraApi.get(`/profile/blocks`, { take, skip });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getUserBlocks", getUserBlocks);
|
@ -14,7 +14,7 @@ export const getUserFriends = async (
|
|||||||
return HydraApi.get(`/profile/friends`, { take, skip });
|
return HydraApi.get(`/profile/friends`, { take, skip });
|
||||||
}
|
}
|
||||||
|
|
||||||
return HydraApi.get(`/user/${userId}/friends`, { take, skip });
|
return HydraApi.get(`/users/${userId}/friends`, { take, skip });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserFriendsEvent = async (
|
const getUserFriendsEvent = async (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { UserProfile } from "@types";
|
import { GameRunning, UserGame, UserProfile } from "@types";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
import { getSteamAppAsset } from "@main/helpers";
|
||||||
import { getUserFriends } from "./get-user-friends";
|
import { getUserFriends } from "./get-user-friends";
|
||||||
@ -12,7 +12,7 @@ const getUser = async (
|
|||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
try {
|
||||||
const [profile, friends] = await Promise.all([
|
const [profile, friends] = await Promise.all([
|
||||||
HydraApi.get(`/user/${userId}`),
|
HydraApi.get(`/users/${userId}`),
|
||||||
getUserFriends(userId, 12, 0).catch(() => {
|
getUserFriends(userId, 12, 0).catch(() => {
|
||||||
return { totalFriends: 0, friends: [] };
|
return { totalFriends: 0, friends: [] };
|
||||||
}),
|
}),
|
||||||
@ -20,48 +20,57 @@ const getUser = async (
|
|||||||
|
|
||||||
const recentGames = await Promise.all(
|
const recentGames = await Promise.all(
|
||||||
profile.recentGames.map(async (game) => {
|
profile.recentGames.map(async (game) => {
|
||||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
return getSteamUserGame(game);
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...convertSteamGameToCatalogueEntry(steamGame),
|
|
||||||
iconUrl,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const libraryGames = await Promise.all(
|
const libraryGames = await Promise.all(
|
||||||
profile.libraryGames.map(async (game) => {
|
profile.libraryGames.map(async (game) => {
|
||||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
return getSteamUserGame(game);
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...convertSteamGameToCatalogueEntry(steamGame),
|
|
||||||
iconUrl,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentGame = await getGameRunning(profile.currentGame);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...profile,
|
...profile,
|
||||||
libraryGames,
|
libraryGames,
|
||||||
recentGames,
|
recentGames,
|
||||||
friends: friends.friends,
|
friends: friends.friends,
|
||||||
totalFriends: friends.totalFriends,
|
totalFriends: friends.totalFriends,
|
||||||
|
currentGame,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGameRunning = async (currentGame): Promise<GameRunning | null> => {
|
||||||
|
if (!currentGame) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameRunning = await getSteamUserGame(currentGame);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...gameRunning,
|
||||||
|
sessionDurationInMillis: currentGame.sessionDurationInSeconds * 1000,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSteamUserGame = async (game): Promise<UserGame> => {
|
||||||
|
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||||
|
name: "getById",
|
||||||
|
});
|
||||||
|
const iconUrl = steamGame?.clientIcon
|
||||||
|
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...game,
|
||||||
|
...convertSteamGameToCatalogueEntry(steamGame),
|
||||||
|
iconUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getUser", getUser);
|
registerEvent("getUser", getUser);
|
||||||
|
@ -5,7 +5,7 @@ const unblockUser = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
await HydraApi.post(`/user/${userId}/unblock`);
|
await HydraApi.post(`/users/${userId}/unblock`);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("unblockUser", unblockUser);
|
registerEvent("unblockUser", unblockUser);
|
||||||
|
@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
|
|||||||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
||||||
(download) => ({
|
(download) => ({
|
||||||
title: download.title,
|
title: download.title,
|
||||||
magnet: download.uris[0],
|
uris: JSON.stringify(download.uris),
|
||||||
|
magnet: download.uris[0]!,
|
||||||
fileSize: download.fileSize,
|
fileSize: download.fileSize,
|
||||||
repacker: downloadSource.name,
|
repacker: downloadSource.name,
|
||||||
uploadDate: download.uploadDate,
|
uploadDate: download.uploadDate,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
import UserAgent from "user-agents";
|
import UserAgent from "user-agents";
|
||||||
|
|
||||||
export const getSteamAppAsset = (
|
export const getSteamAppAsset = (
|
||||||
@ -48,13 +49,19 @@ export const sleep = (ms: number) =>
|
|||||||
export const requestWebPage = async (url: string) => {
|
export const requestWebPage = async (url: string) => {
|
||||||
const userAgent = new UserAgent();
|
const userAgent = new UserAgent();
|
||||||
|
|
||||||
return axios
|
const data = await axios
|
||||||
.get(url, {
|
.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": userAgent.toString(),
|
"User-Agent": userAgent.toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
|
||||||
|
const { window } = new JSDOM(data);
|
||||||
|
return window.document;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isPortableVersion = () =>
|
||||||
|
process.env.PORTABLE_EXECUTABLE_FILE != null;
|
||||||
|
|
||||||
export * from "./download-source";
|
export * from "./download-source";
|
||||||
|
@ -20,8 +20,6 @@ autoUpdater.setFeedURL({
|
|||||||
|
|
||||||
autoUpdater.logger = logger;
|
autoUpdater.logger = logger;
|
||||||
|
|
||||||
logger.log("Init Hydra");
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) app.quit();
|
if (!gotTheLock) app.quit();
|
||||||
|
|
||||||
@ -123,7 +121,6 @@ app.on("window-all-closed", () => {
|
|||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.kill();
|
PythonInstance.kill();
|
||||||
logger.log("Quit Hydra");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class FixRepackUploadDate1715900413313 implements MigrationInterface {
|
|
||||||
public async up(_: QueryRunner): Promise<void> {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(_: QueryRunner): Promise<void> {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import { Game } from "@main/entity";
|
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class AlterLastTimePlayedToDatime1716776027208
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// 2024-05-27 02:08:17
|
|
||||||
// Mon, 27 May 2024 02:08:17 GMT
|
|
||||||
const updateLastTimePlayedValues = `
|
|
||||||
UPDATE game SET lastTimePlayed = (SELECT
|
|
||||||
SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year
|
|
||||||
CASE SUBSTR(lastTimePlayed, 9, 3)
|
|
||||||
WHEN 'Jan' THEN '01'
|
|
||||||
WHEN 'Feb' THEN '02'
|
|
||||||
WHEN 'Mar' THEN '03'
|
|
||||||
WHEN 'Apr' THEN '04'
|
|
||||||
WHEN 'May' THEN '05'
|
|
||||||
WHEN 'Jun' THEN '06'
|
|
||||||
WHEN 'Jul' THEN '07'
|
|
||||||
WHEN 'Aug' THEN '08'
|
|
||||||
WHEN 'Sep' THEN '09'
|
|
||||||
WHEN 'Oct' THEN '10'
|
|
||||||
WHEN 'Nov' THEN '11'
|
|
||||||
WHEN 'Dec' THEN '12'
|
|
||||||
END || '-' || -- Month
|
|
||||||
SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day
|
|
||||||
SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss;
|
|
||||||
FROM game)
|
|
||||||
WHERE lastTimePlayed IS NOT NULL;
|
|
||||||
`;
|
|
||||||
|
|
||||||
await queryRunner.query(updateLastTimePlayedValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game");
|
|
||||||
|
|
||||||
const result = await queryBuilder.getMany();
|
|
||||||
|
|
||||||
for (const game of result) {
|
|
||||||
if (!game.lastTimePlayed) continue;
|
|
||||||
await queryRunner.query(
|
|
||||||
`UPDATE game set lastTimePlayed = ? WHERE id = ?;`,
|
|
||||||
[game.lastTimePlayed.toUTCString(), game.id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
50
src/main/migrations/1724081695967-Hydra_2_0_3.ts
Normal file
50
src/main/migrations/1724081695967-Hydra_2_0_3.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class Hydra2031724081695967 implements MigrationInterface {
|
||||||
|
name = 'Hydra2031724081695967'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_source" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" text, "name" text NOT NULL, "etag" text, "downloadCount" integer NOT NULL DEFAULT (0), "status" text NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_aec2879321a87e9bb2ed477981a" UNIQUE ("url"))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_preferences" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "downloadsPath" text, "language" text NOT NULL DEFAULT ('en'), "realDebridApiToken" text, "downloadNotificationsEnabled" boolean NOT NULL DEFAULT (0), "repackUpdatesNotificationsEnabled" boolean NOT NULL DEFAULT (0), "preferQuitInsteadOfHiding" boolean NOT NULL DEFAULT (0), "runAtStartup" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game_shop_cache" ("objectID" text PRIMARY KEY NOT NULL, "shop" text NOT NULL, "serializedData" text, "howLongToBeatSerializedData" text, "language" text, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "userId" text NOT NULL DEFAULT (''), "displayName" text NOT NULL DEFAULT (''), "profileImageUrl" text, "accessToken" text NOT NULL DEFAULT (''), "refreshToken" text NOT NULL DEFAULT (''), "tokenExpirationTimestamp" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"), CONSTRAINT "FK_0c1d6445ad047d9bbd256f961f6" FOREIGN KEY ("repackId") REFERENCES "repack" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||||
|
await queryRunner.query(`INSERT INTO "temporary_game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "game"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "game"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_game" RENAME TO "game"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
|
||||||
|
await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "repack"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"), CONSTRAINT "FK_aed852c94d9ded617a7a07f5415" FOREIGN KEY ("gameId") REFERENCES "game" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||||
|
await queryRunner.query(`INSERT INTO "temporary_download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "download_queue"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "download_queue"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_download_queue" RENAME TO "download_queue"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "download_queue" RENAME TO "temporary_download_queue"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`);
|
||||||
|
await queryRunner.query(`INSERT INTO "download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "temporary_download_queue"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_download_queue"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`);
|
||||||
|
await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_repack"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "game" RENAME TO "temporary_game"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`);
|
||||||
|
await queryRunner.query(`INSERT INTO "game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "temporary_game"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_game"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_auth"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "download_queue"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "game_shop_cache"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_preferences"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "repack"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "download_source"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "game"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
src/main/migrations/1724081984535-DowloadsRefactor.ts
Normal file
20
src/main/migrations/1724081984535-DowloadsRefactor.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class DowloadsRefactor1724081984535 implements MigrationInterface {
|
||||||
|
name = 'DowloadsRefactor1724081984535'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, "uris" text NOT NULL DEFAULT ('[]'), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
|
||||||
|
await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "repack"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
|
||||||
|
await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_repack"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,2 @@
|
|||||||
import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate";
|
export * from "./1724081695967-Hydra_2_0_3";
|
||||||
import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime";
|
export * from "./1724081984535-DowloadsRefactor";
|
||||||
|
|
||||||
export default [
|
|
||||||
FixRepackUploadDate1715900413313,
|
|
||||||
AlterLastTimePlayedToDatime1716776027208,
|
|
||||||
];
|
|
||||||
|
@ -6,8 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
|
|||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||||
import type { DownloadProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
import { GofileApi } from "../hosters";
|
import { GofileApi, QiwiApi } from "../hosters";
|
||||||
import { GenericHTTPDownloader } from "./generic-http-downloader";
|
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static currentDownloader: Downloader | null = null;
|
private static currentDownloader: Downloader | null = null;
|
||||||
@ -20,7 +20,7 @@ export class DownloadManager {
|
|||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
status = await RealDebridDownloader.getStatus();
|
status = await RealDebridDownloader.getStatus();
|
||||||
} else {
|
} else {
|
||||||
status = await GenericHTTPDownloader.getStatus();
|
status = await GenericHttpDownloader.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
@ -71,7 +71,7 @@ export class DownloadManager {
|
|||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
await RealDebridDownloader.pauseDownload();
|
await RealDebridDownloader.pauseDownload();
|
||||||
} else {
|
} else {
|
||||||
await GenericHTTPDownloader.pauseDownload();
|
await GenericHttpDownloader.pauseDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -88,7 +88,7 @@ export class DownloadManager {
|
|||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
RealDebridDownloader.cancelDownload(gameId);
|
RealDebridDownloader.cancelDownload(gameId);
|
||||||
} else {
|
} else {
|
||||||
GenericHTTPDownloader.cancelDownload(gameId);
|
GenericHttpDownloader.cancelDownload(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -96,26 +96,38 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
if (game.downloader === Downloader.Gofile) {
|
switch (game.downloader) {
|
||||||
const id = game!.uri!.split("/").pop();
|
case Downloader.Gofile: {
|
||||||
|
const id = game!.uri!.split("/").pop();
|
||||||
|
|
||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
|
|
||||||
GenericHTTPDownloader.startDownload(game, downloadLink, {
|
GenericHttpDownloader.startDownload(game, downloadLink, {
|
||||||
Cookie: `accountToken=${token}`,
|
Cookie: `accountToken=${token}`,
|
||||||
});
|
});
|
||||||
} else if (game.downloader === Downloader.PixelDrain) {
|
break;
|
||||||
const id = game!.uri!.split("/").pop();
|
}
|
||||||
|
case Downloader.PixelDrain: {
|
||||||
|
const id = game!.uri!.split("/").pop();
|
||||||
|
|
||||||
await GenericHTTPDownloader.startDownload(
|
await GenericHttpDownloader.startDownload(
|
||||||
game,
|
game,
|
||||||
`https://pixeldrain.com/api/file/${id}?download`
|
`https://pixeldrain.com/api/file/${id}?download`
|
||||||
);
|
);
|
||||||
} else if (game.downloader === Downloader.Torrent) {
|
break;
|
||||||
PythonInstance.startDownload(game);
|
}
|
||||||
} else if (game.downloader === Downloader.RealDebrid) {
|
case Downloader.Qiwi: {
|
||||||
RealDebridDownloader.startDownload(game);
|
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
||||||
|
|
||||||
|
await GenericHttpDownloader.startDownload(game, downloadUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Downloader.Torrent:
|
||||||
|
PythonInstance.startDownload(game);
|
||||||
|
break;
|
||||||
|
case Downloader.RealDebrid:
|
||||||
|
RealDebridDownloader.startDownload(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentDownloader = game.downloader;
|
this.currentDownloader = game.downloader;
|
||||||
|
@ -4,14 +4,14 @@ import { calculateETA } from "./helpers";
|
|||||||
import { DownloadProgress } from "@types";
|
import { DownloadProgress } from "@types";
|
||||||
import { HttpDownload } from "./http-download";
|
import { HttpDownload } from "./http-download";
|
||||||
|
|
||||||
export class GenericHTTPDownloader {
|
export class GenericHttpDownloader {
|
||||||
private static downloads = new Map<number, string>();
|
public static downloads = new Map<number, HttpDownload>();
|
||||||
private static downloadingGame: Game | null = null;
|
public static downloadingGame: Game | null = null;
|
||||||
|
|
||||||
public static async getStatus() {
|
public static async getStatus() {
|
||||||
if (this.downloadingGame) {
|
if (this.downloadingGame) {
|
||||||
const gid = this.downloads.get(this.downloadingGame.id)!;
|
const download = this.downloads.get(this.downloadingGame.id)!;
|
||||||
const status = HttpDownload.getStatus(gid);
|
const status = download.getStatus();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const progress =
|
const progress =
|
||||||
@ -57,10 +57,10 @@ export class GenericHTTPDownloader {
|
|||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
if (this.downloadingGame) {
|
if (this.downloadingGame) {
|
||||||
const gid = this.downloads.get(this.downloadingGame!.id!);
|
const httpDownload = this.downloads.get(this.downloadingGame!.id!);
|
||||||
|
|
||||||
if (gid) {
|
if (httpDownload) {
|
||||||
await HttpDownload.pauseDownload(gid);
|
await httpDownload.pauseDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadingGame = null;
|
this.downloadingGame = null;
|
||||||
@ -79,29 +79,31 @@ export class GenericHTTPDownloader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gid = await HttpDownload.startDownload(
|
const httpDownload = new HttpDownload(
|
||||||
game.downloadPath!,
|
game.downloadPath!,
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
|
|
||||||
this.downloads.set(game.id!, gid);
|
httpDownload.startDownload();
|
||||||
|
|
||||||
|
this.downloads.set(game.id!, httpDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
static async cancelDownload(gameId: number) {
|
||||||
const gid = this.downloads.get(gameId);
|
const httpDownload = this.downloads.get(gameId);
|
||||||
|
|
||||||
if (gid) {
|
if (httpDownload) {
|
||||||
await HttpDownload.cancelDownload(gid);
|
await httpDownload.cancelDownload();
|
||||||
this.downloads.delete(gameId);
|
this.downloads.delete(gameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(gameId: number) {
|
static async resumeDownload(gameId: number) {
|
||||||
const gid = this.downloads.get(gameId);
|
const httpDownload = this.downloads.get(gameId);
|
||||||
|
|
||||||
if (gid) {
|
if (httpDownload) {
|
||||||
await HttpDownload.resumeDownload(gid);
|
await httpDownload.resumeDownload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +1,52 @@
|
|||||||
import { DownloadItem } from "electron";
|
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
export class HttpDownload {
|
export class HttpDownload {
|
||||||
private static id = 0;
|
private downloadItem: Electron.DownloadItem;
|
||||||
|
|
||||||
private static downloads: Record<string, DownloadItem> = {};
|
constructor(
|
||||||
|
private downloadPath: string,
|
||||||
|
private downloadUrl: string,
|
||||||
|
private headers?: Record<string, string>
|
||||||
|
) {}
|
||||||
|
|
||||||
public static getStatus(gid: string): {
|
public getStatus() {
|
||||||
completedLength: number;
|
return {
|
||||||
totalLength: number;
|
completedLength: this.downloadItem.getReceivedBytes(),
|
||||||
downloadSpeed: number;
|
totalLength: this.downloadItem.getTotalBytes(),
|
||||||
folderName: string;
|
downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
|
||||||
} | null {
|
folderName: this.downloadItem.getFilename(),
|
||||||
const downloadItem = this.downloads[gid];
|
};
|
||||||
if (downloadItem) {
|
|
||||||
return {
|
|
||||||
completedLength: downloadItem.getReceivedBytes(),
|
|
||||||
totalLength: downloadItem.getTotalBytes(),
|
|
||||||
downloadSpeed: downloadItem.getCurrentBytesPerSecond(),
|
|
||||||
folderName: downloadItem.getFilename(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gid: string) {
|
async cancelDownload() {
|
||||||
const downloadItem = this.downloads[gid];
|
this.downloadItem.cancel();
|
||||||
downloadItem?.cancel();
|
|
||||||
delete this.downloads[gid];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload(gid: string) {
|
async pauseDownload() {
|
||||||
const downloadItem = this.downloads[gid];
|
this.downloadItem.pause();
|
||||||
downloadItem?.pause();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(gid: string) {
|
async resumeDownload() {
|
||||||
const downloadItem = this.downloads[gid];
|
this.downloadItem.resume();
|
||||||
downloadItem?.resume();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(
|
async startDownload() {
|
||||||
downloadPath: string,
|
return new Promise((resolve) => {
|
||||||
downloadUrl: string,
|
const options = this.headers ? { headers: this.headers } : {};
|
||||||
headers?: Record<string, string>
|
WindowManager.mainWindow?.webContents.downloadURL(
|
||||||
) {
|
this.downloadUrl,
|
||||||
return new Promise<string>((resolve) => {
|
options
|
||||||
const options = headers ? { headers } : {};
|
);
|
||||||
WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options);
|
|
||||||
|
|
||||||
const gid = ++this.id;
|
WindowManager.mainWindow?.webContents.session.once(
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.session.on(
|
|
||||||
"will-download",
|
"will-download",
|
||||||
(_event, item, _webContents) => {
|
(_event, item, _webContents) => {
|
||||||
this.downloads[gid.toString()] = item;
|
this.downloadItem = item;
|
||||||
|
|
||||||
// Set the save path, making Electron not to prompt a save dialog.
|
item.setSavePath(path.join(this.downloadPath, item.getFilename()));
|
||||||
item.setSavePath(path.join(downloadPath, item.getFilename()));
|
|
||||||
|
|
||||||
resolve(gid.toString());
|
resolve(null);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
LibtorrentPayload,
|
LibtorrentPayload,
|
||||||
ProcessPayload,
|
ProcessPayload,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { pythonInstanceLogger as logger } from "../logger";
|
||||||
|
|
||||||
export class PythonInstance {
|
export class PythonInstance {
|
||||||
private static pythonProcess: cp.ChildProcess | null = null;
|
private static pythonProcess: cp.ChildProcess | null = null;
|
||||||
@ -32,11 +33,13 @@ export class PythonInstance {
|
|||||||
});
|
});
|
||||||
|
|
||||||
public static spawn(args?: StartDownloadPayload) {
|
public static spawn(args?: StartDownloadPayload) {
|
||||||
|
logger.log("spawning python process with args:", args);
|
||||||
this.pythonProcess = startRPCClient(args);
|
this.pythonProcess = startRPCClient(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static kill() {
|
public static kill() {
|
||||||
if (this.pythonProcess) {
|
if (this.pythonProcess) {
|
||||||
|
logger.log("killing python process");
|
||||||
this.pythonProcess.kill();
|
this.pythonProcess.kill();
|
||||||
this.pythonProcess = null;
|
this.pythonProcess = null;
|
||||||
this.downloadingGameId = -1;
|
this.downloadingGameId = -1;
|
||||||
@ -45,6 +48,7 @@ export class PythonInstance {
|
|||||||
|
|
||||||
public static killTorrent() {
|
public static killTorrent() {
|
||||||
if (this.pythonProcess) {
|
if (this.pythonProcess) {
|
||||||
|
logger.log("killing torrent in python process");
|
||||||
this.rpc.post("/action", { action: "kill-torrent" });
|
this.rpc.post("/action", { action: "kill-torrent" });
|
||||||
this.downloadingGameId = -1;
|
this.downloadingGameId = -1;
|
||||||
}
|
}
|
||||||
@ -138,12 +142,14 @@ export class PythonInstance {
|
|||||||
save_path: game.downloadPath!,
|
save_path: game.downloadPath!,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.rpc.post("/action", {
|
await this.rpc
|
||||||
action: "start",
|
.post("/action", {
|
||||||
game_id: game.id,
|
action: "start",
|
||||||
magnet: game.uri,
|
game_id: game.id,
|
||||||
save_path: game.downloadPath,
|
magnet: game.uri,
|
||||||
} as StartDownloadPayload);
|
save_path: game.downloadPath,
|
||||||
|
} as StartDownloadPayload)
|
||||||
|
.catch(this.handleRpcError);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadingGameId = game.id;
|
this.downloadingGameId = game.id;
|
||||||
@ -159,4 +165,14 @@ export class PythonInstance {
|
|||||||
|
|
||||||
this.downloadingGameId = -1;
|
this.downloadingGameId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async handleRpcError(_error: unknown) {
|
||||||
|
await this.rpc.get("/healthcheck").catch(() => {
|
||||||
|
logger.error(
|
||||||
|
"RPC healthcheck failed. Killing process and starting again"
|
||||||
|
);
|
||||||
|
this.kill();
|
||||||
|
this.spawn();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { RealDebridClient } from "../real-debrid";
|
import { RealDebridClient } from "../real-debrid";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { calculateETA } from "./helpers";
|
|
||||||
import { DownloadProgress } from "@types";
|
|
||||||
import { HttpDownload } from "./http-download";
|
import { HttpDownload } from "./http-download";
|
||||||
|
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||||
|
|
||||||
export class RealDebridDownloader {
|
export class RealDebridDownloader extends GenericHttpDownloader {
|
||||||
private static downloads = new Map<number, string>();
|
|
||||||
private static downloadingGame: Game | null = null;
|
|
||||||
|
|
||||||
private static realDebridTorrentId: string | null = null;
|
private static realDebridTorrentId: string | null = null;
|
||||||
|
|
||||||
private static async getRealDebridDownloadUrl() {
|
private static async getRealDebridDownloadUrl() {
|
||||||
@ -48,66 +43,6 @@ export class RealDebridDownloader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
|
||||||
if (this.downloadingGame) {
|
|
||||||
const gid = this.downloads.get(this.downloadingGame.id)!;
|
|
||||||
const status = HttpDownload.getStatus(gid);
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
const progress =
|
|
||||||
Number(status.completedLength) / Number(status.totalLength);
|
|
||||||
|
|
||||||
await gameRepository.update(
|
|
||||||
{ id: this.downloadingGame!.id },
|
|
||||||
{
|
|
||||||
bytesDownloaded: Number(status.completedLength),
|
|
||||||
fileSize: Number(status.totalLength),
|
|
||||||
progress,
|
|
||||||
status: "active",
|
|
||||||
folderName: status.folderName,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
numPeers: 0,
|
|
||||||
numSeeds: 0,
|
|
||||||
downloadSpeed: Number(status.downloadSpeed),
|
|
||||||
timeRemaining: calculateETA(
|
|
||||||
Number(status.totalLength),
|
|
||||||
Number(status.completedLength),
|
|
||||||
Number(status.downloadSpeed)
|
|
||||||
),
|
|
||||||
isDownloadingMetadata: false,
|
|
||||||
isCheckingFiles: false,
|
|
||||||
progress,
|
|
||||||
gameId: this.downloadingGame!.id,
|
|
||||||
} as DownloadProgress;
|
|
||||||
|
|
||||||
if (progress === 1) {
|
|
||||||
this.downloads.delete(this.downloadingGame.id);
|
|
||||||
this.realDebridTorrentId = null;
|
|
||||||
this.downloadingGame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async pauseDownload() {
|
|
||||||
if (this.downloadingGame) {
|
|
||||||
const gid = this.downloads.get(this.downloadingGame.id);
|
|
||||||
if (gid) {
|
|
||||||
await HttpDownload.pauseDownload(gid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.realDebridTorrentId = null;
|
|
||||||
this.downloadingGame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
if (this.downloads.has(game.id)) {
|
if (this.downloads.has(game.id)) {
|
||||||
await this.resumeDownload(game.id!);
|
await this.resumeDownload(game.id!);
|
||||||
@ -128,32 +63,10 @@ export class RealDebridDownloader {
|
|||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
|
|
||||||
const gid = await HttpDownload.startDownload(
|
const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
|
||||||
game.downloadPath!,
|
httpDownload.startDownload();
|
||||||
downloadUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
this.downloads.set(game.id!, gid);
|
this.downloads.set(game.id!, httpDownload);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
|
||||||
const gid = this.downloads.get(gameId);
|
|
||||||
|
|
||||||
if (gid) {
|
|
||||||
await HttpDownload.cancelDownload(gid);
|
|
||||||
this.downloads.delete(gameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.realDebridTorrentId = null;
|
|
||||||
this.downloadingGame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async resumeDownload(gameId: number) {
|
|
||||||
const gid = this.downloads.get(gameId);
|
|
||||||
|
|
||||||
if (gid) {
|
|
||||||
await HttpDownload.resumeDownload(gid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import crypto from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { app, dialog } from "electron";
|
import { app, dialog } from "electron";
|
||||||
import type { StartDownloadPayload } from "./types";
|
import type { StartDownloadPayload } from "./types";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
import { pythonInstanceLogger as logger } from "../logger";
|
||||||
|
|
||||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||||
darwin: "hydra-download-manager",
|
darwin: "hydra-download-manager",
|
||||||
@ -15,6 +17,13 @@ export const BITTORRENT_PORT = "5881";
|
|||||||
export const RPC_PORT = "8084";
|
export const RPC_PORT = "8084";
|
||||||
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
const logStderr = (readable: Readable | null) => {
|
||||||
|
if (!readable) return;
|
||||||
|
|
||||||
|
readable.setEncoding("utf-8");
|
||||||
|
readable.on("data", logger.log);
|
||||||
|
};
|
||||||
|
|
||||||
export const startTorrentClient = (args?: StartDownloadPayload) => {
|
export const startTorrentClient = (args?: StartDownloadPayload) => {
|
||||||
const commonArgs = [
|
const commonArgs = [
|
||||||
BITTORRENT_PORT,
|
BITTORRENT_PORT,
|
||||||
@ -40,10 +49,14 @@ export const startTorrentClient = (args?: StartDownloadPayload) => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cp.spawn(binaryPath, commonArgs, {
|
const childProcess = cp.spawn(binaryPath, commonArgs, {
|
||||||
stdio: "inherit",
|
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
stdio: ["inherit", "inherit"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logStderr(childProcess.stderr);
|
||||||
|
|
||||||
|
return childProcess;
|
||||||
} else {
|
} else {
|
||||||
const scriptPath = path.join(
|
const scriptPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
@ -53,8 +66,12 @@ export const startTorrentClient = (args?: StartDownloadPayload) => {
|
|||||||
"main.py"
|
"main.py"
|
||||||
);
|
);
|
||||||
|
|
||||||
return cp.spawn("python3", [scriptPath, ...commonArgs], {
|
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
|
||||||
stdio: "inherit",
|
stdio: ["inherit", "inherit"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logStderr(childProcess.stderr);
|
||||||
|
|
||||||
|
return childProcess;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,8 @@ export interface GofileContentsResponse {
|
|||||||
children: Record<string, GofileContentChild>;
|
children: Record<string, GofileContentChild>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WT = "4fd6sg89d7s6";
|
||||||
|
|
||||||
export class GofileApi {
|
export class GofileApi {
|
||||||
private static token: string;
|
private static token: string;
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ export class GofileApi {
|
|||||||
|
|
||||||
public static async getDownloadLink(id: string) {
|
public static async getDownloadLink(id: string) {
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
wt: "4fd6sg89d7s6",
|
wt: WT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await axios.get<{
|
const response = await axios.get<{
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from "./gofile";
|
export * from "./gofile";
|
||||||
|
export * from "./qiwi";
|
||||||
|
15
src/main/services/hosters/qiwi.ts
Normal file
15
src/main/services/hosters/qiwi.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { requestWebPage } from "@main/helpers";
|
||||||
|
|
||||||
|
export class QiwiApi {
|
||||||
|
public static async getDownloadUrl(url: string) {
|
||||||
|
const document = await requestWebPage(url);
|
||||||
|
const fileName = document.querySelector("h1")?.textContent;
|
||||||
|
|
||||||
|
const slug = url.split("/").pop();
|
||||||
|
const extension = fileName?.split(".").pop();
|
||||||
|
|
||||||
|
const downloadUrl = `https://spyderrock.com/${slug}.${extension}`;
|
||||||
|
|
||||||
|
return downloadUrl;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
|
||||||
import { requestWebPage } from "@main/helpers";
|
import { requestWebPage } from "@main/helpers";
|
||||||
import { HowLongToBeatCategory } from "@types";
|
import { HowLongToBeatCategory } from "@types";
|
||||||
import { formatName } from "@shared";
|
import { formatName } from "@shared";
|
||||||
@ -52,10 +51,7 @@ const parseListItems = ($lis: Element[]) => {
|
|||||||
export const getHowLongToBeatGame = async (
|
export const getHowLongToBeatGame = async (
|
||||||
id: string
|
id: string
|
||||||
): Promise<HowLongToBeatCategory[]> => {
|
): Promise<HowLongToBeatCategory[]> => {
|
||||||
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||||
|
|
||||||
const { window } = new JSDOM(response);
|
|
||||||
const { document } = window;
|
|
||||||
|
|
||||||
const $ul = document.querySelector(".shadow_shadow ul");
|
const $ul = document.querySelector(".shadow_shadow ul");
|
||||||
if (!$ul) return [];
|
if (!$ul) return [];
|
||||||
|
@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api";
|
|||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
export const createGame = async (game: Game) => {
|
export const createGame = async (game: Game) => {
|
||||||
HydraApi.post(`/games`, {
|
HydraApi.post(`/profile/games`, {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
|
@ -4,7 +4,7 @@ import { steamGamesWorker } from "@main/workers";
|
|||||||
import { getSteamAppAsset } from "@main/helpers";
|
import { getSteamAppAsset } from "@main/helpers";
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
return HydraApi.get("/games")
|
return HydraApi.get("/profile/games")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
for (const game of response) {
|
for (const game of response) {
|
||||||
const localGame = await gameRepository.findOne({
|
const localGame = await gameRepository.findOne({
|
||||||
|
@ -6,7 +6,7 @@ export const updateGamePlaytime = async (
|
|||||||
deltaInMillis: number,
|
deltaInMillis: number,
|
||||||
lastTimePlayed: Date
|
lastTimePlayed: Date
|
||||||
) => {
|
) => {
|
||||||
HydraApi.put(`/games/${game.remoteId}`, {
|
HydraApi.put(`/profile/games/${game.remoteId}`, {
|
||||||
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
||||||
lastTimePlayed,
|
lastTimePlayed,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
@ -14,7 +14,7 @@ export const uploadGamesBatch = async () => {
|
|||||||
|
|
||||||
for (const chunk of gamesChunks) {
|
for (const chunk of gamesChunks) {
|
||||||
await HydraApi.post(
|
await HydraApi.post(
|
||||||
"/games/batch",
|
"/profile/games/batch",
|
||||||
chunk.map((game) => {
|
chunk.map((game) => {
|
||||||
return {
|
return {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
|
@ -6,6 +6,10 @@ log.transports.file.resolvePathFn = (
|
|||||||
_: log.PathVariables,
|
_: log.PathVariables,
|
||||||
message?: log.LogMessage | undefined
|
message?: log.LogMessage | undefined
|
||||||
) => {
|
) => {
|
||||||
|
if (message?.scope === "python-instance") {
|
||||||
|
return path.join(logsPath, "pythoninstance.txt");
|
||||||
|
}
|
||||||
|
|
||||||
if (message?.level === "error") {
|
if (message?.level === "error") {
|
||||||
return path.join(logsPath, "error.txt");
|
return path.join(logsPath, "error.txt");
|
||||||
}
|
}
|
||||||
@ -23,4 +27,5 @@ log.errorHandler.startCatching({
|
|||||||
|
|
||||||
log.initialize();
|
log.initialize();
|
||||||
|
|
||||||
|
export const pythonInstanceLogger = log.scope("python-instance");
|
||||||
export const logger = log.scope("main");
|
export const logger = log.scope("main");
|
||||||
|
@ -10,6 +10,6 @@ export const startMainLoop = async () => {
|
|||||||
DownloadManager.watchDownloads(),
|
DownloadManager.watchDownloads(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,12 +4,16 @@ import { WindowManager } from "./window-manager";
|
|||||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||||
import { GameRunning } from "@types";
|
import { GameRunning } from "@types";
|
||||||
import { PythonInstance } from "./download";
|
import { PythonInstance } from "./download";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
number,
|
number,
|
||||||
{ lastTick: number; firstTick: number }
|
{ lastTick: number; firstTick: number; lastSyncTick: number }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
const TICKS_TO_UPDATE_API = 120;
|
||||||
|
let currentTick = 1;
|
||||||
|
|
||||||
export const watchProcesses = async () => {
|
export const watchProcesses = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gameRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@ -30,48 +34,17 @@ export const watchProcesses = async () => {
|
|||||||
|
|
||||||
if (gameProcess) {
|
if (gameProcess) {
|
||||||
if (gamesPlaytime.has(game.id)) {
|
if (gamesPlaytime.has(game.id)) {
|
||||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
onTickGame(game);
|
||||||
|
|
||||||
const zero = gamePlaytime.lastTick;
|
|
||||||
const delta = performance.now() - zero;
|
|
||||||
|
|
||||||
await gameRepository.update(game.id, {
|
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
|
||||||
lastTimePlayed: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
gamesPlaytime.set(game.id, {
|
|
||||||
...gamePlaytime,
|
|
||||||
lastTick: performance.now(),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (game.remoteId) {
|
onOpenGame(game);
|
||||||
updateGamePlaytime(game, 0, new Date());
|
|
||||||
} else {
|
|
||||||
createGame({ ...game, lastTimePlayed: new Date() });
|
|
||||||
}
|
|
||||||
|
|
||||||
gamesPlaytime.set(game.id, {
|
|
||||||
lastTick: performance.now(),
|
|
||||||
firstTick: performance.now(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (gamesPlaytime.has(game.id)) {
|
} else if (gamesPlaytime.has(game.id)) {
|
||||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
onCloseGame(game);
|
||||||
gamesPlaytime.delete(game.id);
|
|
||||||
|
|
||||||
if (game.remoteId) {
|
|
||||||
updateGamePlaytime(
|
|
||||||
game,
|
|
||||||
performance.now() - gamePlaytime.firstTick,
|
|
||||||
game.lastTimePlayed!
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createGame(game);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentTick++;
|
||||||
|
|
||||||
if (WindowManager.mainWindow) {
|
if (WindowManager.mainWindow) {
|
||||||
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
|
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
|
||||||
return {
|
return {
|
||||||
@ -86,3 +59,68 @@ export const watchProcesses = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onOpenGame(game: Game) {
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
gamesPlaytime.set(game.id, {
|
||||||
|
lastTick: now,
|
||||||
|
firstTick: now,
|
||||||
|
lastSyncTick: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (game.remoteId) {
|
||||||
|
updateGamePlaytime(game, 0, new Date());
|
||||||
|
} else {
|
||||||
|
createGame({ ...game, lastTimePlayed: new Date() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTickGame(game: Game) {
|
||||||
|
const now = performance.now();
|
||||||
|
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||||
|
|
||||||
|
const delta = now - gamePlaytime.lastTick;
|
||||||
|
|
||||||
|
gameRepository.update(game.id, {
|
||||||
|
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||||
|
lastTimePlayed: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
gamesPlaytime.set(game.id, {
|
||||||
|
...gamePlaytime,
|
||||||
|
lastTick: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentTick % TICKS_TO_UPDATE_API === 0) {
|
||||||
|
if (game.remoteId) {
|
||||||
|
updateGamePlaytime(
|
||||||
|
game,
|
||||||
|
now - gamePlaytime.lastSyncTick,
|
||||||
|
game.lastTimePlayed!
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createGame(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
gamesPlaytime.set(game.id, {
|
||||||
|
...gamePlaytime,
|
||||||
|
lastSyncTick: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseGame = (game: Game) => {
|
||||||
|
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||||
|
gamesPlaytime.delete(game.id);
|
||||||
|
|
||||||
|
if (game.remoteId) {
|
||||||
|
updateGamePlaytime(
|
||||||
|
game,
|
||||||
|
performance.now() - gamePlaytime.firstTick,
|
||||||
|
game.lastTimePlayed!
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createGame(game);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -8,11 +8,25 @@ export class RepacksManager {
|
|||||||
private static repacksIndex = new flexSearch.Index();
|
private static repacksIndex = new flexSearch.Index();
|
||||||
|
|
||||||
public static async updateRepacks() {
|
public static async updateRepacks() {
|
||||||
this.repacks = await repackRepository.find({
|
this.repacks = await repackRepository
|
||||||
order: {
|
.find({
|
||||||
createdAt: "DESC",
|
order: {
|
||||||
},
|
createdAt: "DESC",
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
.then((repacks) =>
|
||||||
|
repacks.map((repack) => {
|
||||||
|
const uris: string[] = [];
|
||||||
|
const magnet = repack?.magnet;
|
||||||
|
|
||||||
|
if (magnet) uris.push(magnet);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...repack,
|
||||||
|
uris: [...uris, ...JSON.parse(repack.uris)],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < this.repacks.length; i++) {
|
for (let i = 0; i < this.repacks.length; i++) {
|
||||||
this.repacksIndex.remove(i);
|
this.repacksIndex.remove(i);
|
||||||
|
@ -158,7 +158,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
|
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
|
||||||
games.map(({ title, executablePath }) => ({
|
games.map(({ title, executablePath }) => ({
|
||||||
label: title,
|
label: title.length > 15 ? `${title.slice(0, 15)}…` : title,
|
||||||
type: "normal",
|
type: "normal",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
if (!executablePath) return;
|
if (!executablePath) return;
|
||||||
|
@ -10,6 +10,7 @@ import type {
|
|||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
|
UpdateProfileProps,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -137,8 +138,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
undoFriendship: (userId: string) =>
|
undoFriendship: (userId: string) =>
|
||||||
ipcRenderer.invoke("undoFriendship", userId),
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
updateProfile: (updateProfile: UpdateProfileProps) =>
|
||||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
@ -151,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
||||||
|
getUserBlocks: (take: number, skip: number) =>
|
||||||
|
ipcRenderer.invoke("getUserBlocks", take, skip),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
|
@ -108,7 +108,7 @@ export function App() {
|
|||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
@ -118,7 +118,13 @@ export function App() {
|
|||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
}, [
|
||||||
|
fetchUserDetails,
|
||||||
|
fetchFriendRequests,
|
||||||
|
t,
|
||||||
|
showSuccessToast,
|
||||||
|
updateUserDetails,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
||||||
|
@ -78,7 +78,7 @@ export function SidebarProfile() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{userDetails && gameRunning && (
|
{userDetails && gameRunning?.iconUrl && (
|
||||||
<img
|
<img
|
||||||
alt={gameRunning.title}
|
alt={gameRunning.title}
|
||||||
width={24}
|
width={24}
|
||||||
|
@ -7,4 +7,5 @@ export const DOWNLOADER_NAME = {
|
|||||||
[Downloader.Torrent]: "Torrent",
|
[Downloader.Torrent]: "Torrent",
|
||||||
[Downloader.Gofile]: "Gofile",
|
[Downloader.Gofile]: "Gofile",
|
||||||
[Downloader.PixelDrain]: "PixelDrain",
|
[Downloader.PixelDrain]: "PixelDrain",
|
||||||
|
[Downloader.Qiwi]: "Qiwi",
|
||||||
};
|
};
|
||||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -17,6 +17,7 @@ import type {
|
|||||||
FriendRequest,
|
FriendRequest,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UserFriends,
|
UserFriends,
|
||||||
|
UserBlocks,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -135,14 +136,12 @@ declare global {
|
|||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<UserFriends>;
|
) => Promise<UserFriends>;
|
||||||
|
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (updateProfile: UpdateProfileProps) => Promise<UserProfile>;
|
||||||
displayName: string,
|
|
||||||
newProfileImagePath: string | null
|
|
||||||
) => Promise<UserProfile>;
|
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -22,9 +22,10 @@ export function useDownload() {
|
|||||||
);
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const startDownload = (payload: StartGameDownloadPayload) => {
|
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
window.electron.startGameDownload(payload).then((game) => {
|
|
||||||
|
return window.electron.startGameDownload(payload).then((game) => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||||
import { FriendRequestAction, UserDetails } from "@types";
|
import { FriendRequestAction, UpdateProfileProps, UserDetails } from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
import { logger } from "@renderer/logger";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -43,7 +44,10 @@ export function useUserDetails() {
|
|||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const profileBackground = await profileBackgroundFromProfileImage(
|
const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
userDetails.profileImageUrl
|
userDetails.profileImageUrl
|
||||||
);
|
).catch((err) => {
|
||||||
|
logger.error("profileBackgroundFromProfileImage", err);
|
||||||
|
return `#151515B3`;
|
||||||
|
});
|
||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@ -74,12 +78,8 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (displayName: string, imageProfileUrl: string | null) => {
|
async (props: UpdateProfileProps) => {
|
||||||
const response = await window.electron.updateProfile(
|
const response = await window.electron.updateProfile(props);
|
||||||
displayName,
|
|
||||||
imageProfileUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
return updateUserDetails(response);
|
return updateUserDetails(response);
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
@ -99,7 +99,7 @@ export function useUserDetails() {
|
|||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
fetchFriendRequests();
|
fetchFriendRequests();
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideFriendsModal = useCallback(() => {
|
const hideFriendsModal = useCallback(() => {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
} from "@renderer/context";
|
} from "@renderer/context";
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader, getDownloadersForUri } from "@shared";
|
||||||
|
|
||||||
export function GameDetails() {
|
export function GameDetails() {
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
@ -70,6 +70,9 @@ export function GameDetails() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectRepackUri = (repack: GameRepack, downloader: Downloader) =>
|
||||||
|
repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameDetailsContextProvider>
|
<GameDetailsContextProvider>
|
||||||
<GameDetailsContextConsumer>
|
<GameDetailsContextConsumer>
|
||||||
@ -96,6 +99,7 @@ export function GameDetails() {
|
|||||||
downloader,
|
downloader,
|
||||||
shop: shop as GameShop,
|
shop: shop as GameShop,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
|
uri: selectRepackUri(repack, downloader),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateGame();
|
await updateGame();
|
||||||
|
@ -20,13 +20,16 @@ export const hintText = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const downloaders = style({
|
export const downloaders = style({
|
||||||
display: "flex",
|
display: "grid",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloaderOption = style({
|
export const downloaderOption = style({
|
||||||
flex: "1",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
":only-child": {
|
||||||
|
gridColumn: "1 / -1",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloaderIcon = style({
|
export const downloaderIcon = style({
|
||||||
|
@ -5,7 +5,7 @@ import { DiskSpace } from "check-disk-space";
|
|||||||
import * as styles from "./download-settings-modal.css";
|
import * as styles from "./download-settings-modal.css";
|
||||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||||
import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
|
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||||
|
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
@ -48,8 +48,8 @@ export function DownloadSettingsModal({
|
|||||||
}, [visible, selectedPath]);
|
}, [visible, selectedPath]);
|
||||||
|
|
||||||
const downloaders = useMemo(() => {
|
const downloaders = useMemo(() => {
|
||||||
return getDownloadersForUri(repack?.magnet ?? "");
|
return getDownloadersForUris(repack?.uris ?? []);
|
||||||
}, [repack?.magnet]);
|
}, [repack?.uris]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userPreferences?.downloadsPath) {
|
if (userPreferences?.downloadsPath) {
|
||||||
|
@ -76,6 +76,13 @@ export function RepacksModal({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||||
|
if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
|
||||||
|
if (!game?.uri) return false;
|
||||||
|
|
||||||
|
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DownloadSettingsModal
|
<DownloadSettingsModal
|
||||||
@ -97,9 +104,7 @@ export function RepacksModal({
|
|||||||
|
|
||||||
<div className={styles.repacks}>
|
<div className={styles.repacks}>
|
||||||
{filteredRepacks.map((repack) => {
|
{filteredRepacks.map((repack) => {
|
||||||
const isLastDownloadedOption =
|
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
||||||
infoHash !== null &&
|
|
||||||
repack.magnet.toLowerCase().includes(infoHash);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -42,10 +42,3 @@ export const downloadSourcesHeader = style({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const separator = style({
|
|
||||||
height: "100%",
|
|
||||||
width: "1px",
|
|
||||||
backgroundColor: vars.color.border,
|
|
||||||
margin: `${SPACING_UNIT}px 0`,
|
|
||||||
});
|
|
||||||
|
@ -134,15 +134,6 @@ export function SettingsDownloadSources() {
|
|||||||
downloadSource.downloadCount.toLocaleString(),
|
downloadSource.downloadCount.toLocaleString(),
|
||||||
})}
|
})}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div className={styles.separator} />
|
|
||||||
|
|
||||||
<small>
|
|
||||||
{t("download_options", {
|
|
||||||
count: downloadSource.repackCount,
|
|
||||||
countFormatted: downloadSource.repackCount.toLocaleString(),
|
|
||||||
})}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import * as styles from "./user-friend-modal.css";
|
import * as styles from "./user-friend-modal.css";
|
||||||
import cn from "classnames";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -12,21 +11,26 @@ export type UserFriendItemProps = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
onClickItem: (userId: string) => void;
|
|
||||||
} & (
|
} & (
|
||||||
| { type: "ACCEPTED"; onClickUndoFriendship: (userId: string) => void }
|
| {
|
||||||
|
type: "ACCEPTED";
|
||||||
|
onClickUndoFriendship: (userId: string) => void;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
|
}
|
||||||
|
| { type: "BLOCKED"; onClickUnblock: (userId: string) => void }
|
||||||
| {
|
| {
|
||||||
type: "SENT" | "RECEIVED";
|
type: "SENT" | "RECEIVED";
|
||||||
onClickCancelRequest: (userId: string) => void;
|
onClickCancelRequest: (userId: string) => void;
|
||||||
onClickAcceptRequest: (userId: string) => void;
|
onClickAcceptRequest: (userId: string) => void;
|
||||||
onClickRefuseRequest: (userId: string) => void;
|
onClickRefuseRequest: (userId: string) => void;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
}
|
}
|
||||||
| { type: null }
|
| { type: null; onClickItem: (userId: string) => void }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UserFriendItem = (props: UserFriendItemProps) => {
|
export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { userId, profileImageUrl, displayName, type, onClickItem } = props;
|
const { userId, profileImageUrl, displayName, type } = props;
|
||||||
|
|
||||||
const getRequestDescription = () => {
|
const getRequestDescription = () => {
|
||||||
if (type === "ACCEPTED" || type === null) return null;
|
if (type === "ACCEPTED" || type === null) return null;
|
||||||
@ -86,15 +90,69 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickUnblock(userId)}
|
||||||
|
title={t("unblock")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === "BLOCKED") {
|
||||||
|
return (
|
||||||
|
<div className={styles.friendListContainer}>
|
||||||
|
<div className={styles.friendListButton} style={{ cursor: "inherit" }}>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={displayName}
|
||||||
|
src={profileImageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flex: "1",
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getRequestActions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
<div className={styles.friendListContainer}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendListButton}
|
className={styles.friendListButton}
|
||||||
onClick={() => onClickItem(userId)}
|
onClick={() => props.onClickItem(userId)}
|
||||||
>
|
>
|
||||||
<div className={styles.friendAvatarContainer}>
|
<div className={styles.friendAvatarContainer}>
|
||||||
{profileImageUrl ? (
|
{profileImageUrl ? (
|
||||||
|
@ -40,20 +40,16 @@ export const UserFriendModalAddFriend = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetAndClose = () => {
|
|
||||||
setFriendCode("");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickRequest = (userId: string) => {
|
const handleClickRequest = (userId: string) => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
navigate(`/user/${userId}`);
|
navigate(`/user/${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickSeeProfile = () => {
|
const handleClickSeeProfile = () => {
|
||||||
resetAndClose();
|
closeModal();
|
||||||
// TODO: add validation for this input?
|
if (friendCode.length === 8) {
|
||||||
navigate(`/user/${friendCode}`);
|
navigate(`/user/${friendCode}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelFriendRequest = (userId: string) => {
|
const handleCancelFriendRequest = (userId: string) => {
|
||||||
@ -122,7 +118,8 @@ export const UserFriendModalAddFriend = ({
|
|||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3>Pendentes</h3>
|
<h3>{t("pending")}</h3>
|
||||||
|
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
||||||
{friendRequests.map((request) => {
|
{friendRequests.map((request) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendItem
|
<UserFriendItem
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { UserFriend } from "@types";
|
import { UserFriend } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { UserFriendItem } from "./user-friend-item";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
export interface UserFriendModalListProps {
|
export interface UserFriendModalListProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -22,14 +23,17 @@ export const UserFriendModalList = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
const [friends, setFriends] = useState<UserFriend[]>([]);
|
const [friends, setFriends] = useState<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { userDetails, undoFriendship } = useUserDetails();
|
const { userDetails, undoFriendship } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
const loadNextPage = () => {
|
const loadNextPage = () => {
|
||||||
if (page > maxPage) return;
|
if (page > maxPage) return;
|
||||||
|
setIsLoading(true);
|
||||||
window.electron
|
window.electron
|
||||||
.getUserFriends(userId, pageSize, page * pageSize)
|
.getUserFriends(userId, pageSize, page * pageSize)
|
||||||
.then((newPage) => {
|
.then((newPage) => {
|
||||||
@ -40,9 +44,29 @@ export const UserFriendModalList = ({
|
|||||||
setFriends([...friends, ...newPage.friends]);
|
setFriends([...friends, ...newPage.friends]);
|
||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = listContainer.current?.scrollTop || 0;
|
||||||
|
const scrollHeight = listContainer.current?.scrollHeight || 0;
|
||||||
|
const clientHeight = listContainer.current?.clientHeight || 0;
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (scrollTop < maxScrollTop * 0.9 || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = listContainer.current;
|
||||||
|
container?.addEventListener("scroll", handleScroll);
|
||||||
|
return () => container?.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
const reloadList = () => {
|
const reloadList = () => {
|
||||||
setPage(0);
|
setPage(0);
|
||||||
setMaxPage(0);
|
setMaxPage(0);
|
||||||
@ -70,26 +94,42 @@ export const UserFriendModalList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
style={{
|
<div
|
||||||
display: "flex",
|
ref={listContainer}
|
||||||
flexDirection: "column",
|
style={{
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
display: "flex",
|
||||||
}}
|
flexDirection: "column",
|
||||||
>
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
{friends.map((friend) => {
|
maxHeight: "400px",
|
||||||
return (
|
overflowY: "scroll",
|
||||||
<UserFriendItem
|
}}
|
||||||
userId={friend.id}
|
>
|
||||||
displayName={friend.displayName}
|
{!isLoading && friends.length === 0 && <p>{t("no_friends_added")}</p>}
|
||||||
profileImageUrl={friend.profileImageUrl}
|
{friends.map((friend) => {
|
||||||
onClickItem={handleClickFriend}
|
return (
|
||||||
onClickUndoFriendship={handleUndoFriendship}
|
<UserFriendItem
|
||||||
type={isMe ? "ACCEPTED" : null}
|
userId={friend.id}
|
||||||
key={friend.id}
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickItem={handleClickFriend}
|
||||||
|
onClickUndoFriendship={handleUndoFriendship}
|
||||||
|
type={isMe ? "ACCEPTED" : null}
|
||||||
|
key={"modal" + friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,6 @@
|
|||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const profileContentBox = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
width: "100%",
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
|
||||||
transition: "all ease 0.3s",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const friendAvatarContainer = style({
|
export const friendAvatarContainer = style({
|
||||||
width: "35px",
|
width: "35px",
|
||||||
minWidth: "35px",
|
minWidth: "35px",
|
||||||
@ -42,8 +31,14 @@ export const profileAvatar = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const friendListContainer = style({
|
export const friendListContainer = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "54px",
|
height: "54px",
|
||||||
|
minHeight: "54px",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
":hover": {
|
":hover": {
|
||||||
@ -90,3 +85,15 @@ export const cancelRequestButton = style({
|
|||||||
color: vars.color.danger,
|
color: vars.color.danger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendCodeButton = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { UserFriendModalList } from "./user-friend-modal-list";
|
import { UserFriendModalList } from "./user-friend-modal-list";
|
||||||
|
import { CopyIcon } from "@primer/octicons-react";
|
||||||
|
import * as styles from "./user-friend-modal.css";
|
||||||
|
|
||||||
export enum UserFriendModalTab {
|
export enum UserFriendModalTab {
|
||||||
FriendsList,
|
FriendsList,
|
||||||
@ -32,6 +34,8 @@ export const UserFriendModal = ({
|
|||||||
initialTab || UserFriendModalTab.FriendsList
|
initialTab || UserFriendModalTab.FriendsList
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const { userDetails } = useUserDetails();
|
const { userDetails } = useUserDetails();
|
||||||
const isMe = userDetails?.id == userId;
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
@ -53,6 +57,11 @@ export const UserFriendModal = ({
|
|||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(userDetails!.id);
|
||||||
|
showSuccessToast(t("friend_code_copied"));
|
||||||
|
}, [userDetails, showSuccessToast, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
||||||
<div
|
<div
|
||||||
@ -64,19 +73,37 @@ export const UserFriendModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
<>
|
||||||
{tabs.map((tab, index) => {
|
<div
|
||||||
return (
|
style={{
|
||||||
<Button
|
display: "flex",
|
||||||
key={tab}
|
gap: `${SPACING_UNIT}px`,
|
||||||
theme={index === currentTab ? "primary" : "outline"}
|
alignItems: "center",
|
||||||
onClick={() => setCurrentTab(index)}
|
}}
|
||||||
>
|
>
|
||||||
{tab}
|
<p>Seu código de amigo: </p>
|
||||||
</Button>
|
<button
|
||||||
);
|
className={styles.friendCodeButton}
|
||||||
})}
|
onClick={copyToClipboard}
|
||||||
</section>
|
>
|
||||||
|
<h3>{userDetails.id}</h3>
|
||||||
|
<CopyIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
theme={index === currentTab ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentTab(index)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{renderTab()}
|
{renderTab()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { FriendRequestAction, UserGame, UserProfile } from "@types";
|
import {
|
||||||
|
FriendRequestAction,
|
||||||
|
GameRunning,
|
||||||
|
UserGame,
|
||||||
|
UserProfile,
|
||||||
|
} from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
@ -25,7 +30,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
import { UserEditProfileModal } from "./user-edit-modal";
|
import { UserProfileSettingsModal } from "./user-profile-settings-modal";
|
||||||
import { UserSignOutModal } from "./user-sign-out-modal";
|
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||||
import { UserBlockModal } from "./user-block-modal";
|
import { UserBlockModal } from "./user-block-modal";
|
||||||
@ -44,7 +49,6 @@ export function UserContent({
|
|||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
}: ProfileContentProps) {
|
}: ProfileContentProps) {
|
||||||
const { t, i18n } = useTranslation("user_profile");
|
const { t, i18n } = useTranslation("user_profile");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
@ -60,9 +64,11 @@ export function UserContent({
|
|||||||
|
|
||||||
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||||
useState<string | undefined>();
|
useState<string | undefined>();
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showProfileSettingsModal, setShowProfileSettingsModal] =
|
||||||
|
useState(false);
|
||||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||||
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||||
|
const [currentGame, setCurrentGame] = useState<GameRunning | null>(null);
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
@ -95,7 +101,7 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProfile = () => {
|
const handleEditProfile = () => {
|
||||||
setShowEditProfileModal(true);
|
setShowProfileSettingsModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnClickFriend = (userId: string) => {
|
const handleOnClickFriend = (userId: string) => {
|
||||||
@ -112,9 +118,18 @@ export function UserContent({
|
|||||||
|
|
||||||
const isMe = userDetails?.id == userProfile.id;
|
const isMe = userDetails?.id == userProfile.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMe && gameRunning) {
|
||||||
|
setCurrentGame(gameRunning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentGame(userProfile.currentGame);
|
||||||
|
}, [gameRunning, isMe, userProfile.currentGame]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe) fetchFriendRequests();
|
if (isMe) fetchFriendRequests();
|
||||||
}, [isMe]);
|
}, [isMe, fetchFriendRequests]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe && profileBackground) {
|
if (isMe && profileBackground) {
|
||||||
@ -128,7 +143,7 @@ export function UserContent({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [profileBackground, isMe]);
|
}, [profileBackground, isMe, userProfile.profileImageUrl]);
|
||||||
|
|
||||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||||
try {
|
try {
|
||||||
@ -159,13 +174,18 @@ export function UserContent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showFriends = isMe || userProfile.totalFriends > 0;
|
const showFriends = isMe || userProfile.totalFriends > 0;
|
||||||
|
const showProfileContent =
|
||||||
|
isMe ||
|
||||||
|
userProfile.profileVisibility === "PUBLIC" ||
|
||||||
|
(userProfile.relation?.status === "ACCEPTED" &&
|
||||||
|
userProfile.profileVisibility === "FRIENDS");
|
||||||
|
|
||||||
const getProfileActions = () => {
|
const getProfileActions = () => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button theme="outline" onClick={handleEditProfile}>
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
{t("edit_profile")}
|
{t("settings")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||||
@ -251,9 +271,9 @@ export function UserContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserEditProfileModal
|
<UserProfileSettingsModal
|
||||||
visible={showEditProfileModal}
|
visible={showProfileSettingsModal}
|
||||||
onClose={() => setShowEditProfileModal(false)}
|
onClose={() => setShowProfileSettingsModal(false)}
|
||||||
updateUserProfile={updateUserProfile}
|
updateUserProfile={updateUserProfile}
|
||||||
userProfile={userProfile}
|
userProfile={userProfile}
|
||||||
/>
|
/>
|
||||||
@ -278,10 +298,10 @@ export function UserContent({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{gameRunning && isMe && (
|
{currentGame && (
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(gameRunning.objectID)}
|
src={steamUrlBuilder.libraryHero(currentGame.objectID)}
|
||||||
alt={gameRunning.title}
|
alt={currentGame.title}
|
||||||
className={styles.profileBackground}
|
className={styles.profileBackground}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -309,7 +329,7 @@ export function UserContent({
|
|||||||
|
|
||||||
<div className={styles.profileInformation}>
|
<div className={styles.profileInformation}>
|
||||||
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
|
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
|
||||||
{isMe && gameRunning && (
|
{currentGame && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -325,14 +345,14 @@ export function UserContent({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link to={buildGameDetailsPath(gameRunning)}>
|
<Link to={buildGameDetailsPath(currentGame)}>
|
||||||
{gameRunning.title}
|
{currentGame.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
{t("playing_for", {
|
{t("playing_for", {
|
||||||
amount: formatDiffInMillis(
|
amount: formatDiffInMillis(
|
||||||
gameRunning.sessionDurationInMillis,
|
currentGame.sessionDurationInMillis,
|
||||||
new Date()
|
new Date()
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
@ -361,121 +381,69 @@ export function UserContent({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className={styles.profileContent}>
|
{showProfileContent && (
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileContent}>
|
||||||
<h2>{t("activity")}</h2>
|
|
||||||
|
|
||||||
{!userProfile.recentGames.length ? (
|
|
||||||
<div className={styles.noDownloads}>
|
|
||||||
<div className={styles.telescopeIcon}>
|
|
||||||
<TelescopeIcon size={24} />
|
|
||||||
</div>
|
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
|
||||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{userProfile.recentGames.map((game) => (
|
|
||||||
<button
|
|
||||||
key={game.objectID}
|
|
||||||
className={cn(styles.feedItem, styles.profileContentBox)}
|
|
||||||
onClick={() => handleGameClick(game)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={styles.feedGameIcon}
|
|
||||||
src={game.cover}
|
|
||||||
alt={game.title}
|
|
||||||
/>
|
|
||||||
<div className={styles.gameInformation}>
|
|
||||||
<h4>{game.title}</h4>
|
|
||||||
<small>
|
|
||||||
{t("last_time_played", {
|
|
||||||
period: formatDistance(
|
|
||||||
game.lastTimePlayed!,
|
|
||||||
new Date(),
|
|
||||||
{
|
|
||||||
addSuffix: true,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.contentSidebar}>
|
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<div
|
<h2>{t("activity")}</h2>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2>{t("library")}</h2>
|
|
||||||
|
|
||||||
|
{!userProfile.recentGames.length ? (
|
||||||
|
<div className={styles.noDownloads}>
|
||||||
|
<div className={styles.telescopeIcon}>
|
||||||
|
<TelescopeIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
|
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
display: "flex",
|
||||||
backgroundColor: vars.color.border,
|
flexDirection: "column",
|
||||||
height: "1px",
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
{userProfile.recentGames.map((game) => (
|
||||||
{userProfile.libraryGames.length}
|
<button
|
||||||
</h3>
|
key={game.objectID}
|
||||||
</div>
|
className={cn(styles.feedItem, styles.profileContentBox)}
|
||||||
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
onClick={() => handleGameClick(game)}
|
||||||
<div
|
>
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{userProfile.libraryGames.map((game) => (
|
|
||||||
<button
|
|
||||||
key={game.objectID}
|
|
||||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
|
||||||
onClick={() => handleGameClick(game)}
|
|
||||||
title={game.title}
|
|
||||||
>
|
|
||||||
{game.iconUrl ? (
|
|
||||||
<img
|
<img
|
||||||
className={styles.libraryGameIcon}
|
className={styles.feedGameIcon}
|
||||||
src={game.iconUrl}
|
src={game.cover}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
) : (
|
<div className={styles.gameInformation}>
|
||||||
<SteamLogo className={styles.libraryGameIcon} />
|
<h4>{game.title}</h4>
|
||||||
)}
|
<small>
|
||||||
</button>
|
{t("last_time_played", {
|
||||||
))}
|
period: formatDistance(
|
||||||
</div>
|
game.lastTimePlayed!,
|
||||||
|
new Date(),
|
||||||
|
{
|
||||||
|
addSuffix: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showFriends && (
|
<div className={styles.contentSidebar}>
|
||||||
<div className={styles.friendsSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<button
|
<div
|
||||||
className={styles.friendsSectionHeader}
|
style={{
|
||||||
onClick={() =>
|
display: "flex",
|
||||||
showFriendsModal(
|
alignItems: "center",
|
||||||
UserFriendModalTab.FriendsList,
|
justifyContent: "space-between",
|
||||||
userProfile.id
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<h2>{t("friends")}</h2>
|
<h2>{t("library")}</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -485,64 +453,123 @@ export function UserContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
{userProfile.totalFriends}
|
{userProfile.libraryGames.length}
|
||||||
</h3>
|
</h3>
|
||||||
</button>
|
</div>
|
||||||
|
<small>
|
||||||
|
{t("total_play_time", { amount: formatPlayTime() })}
|
||||||
|
</small>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "grid",
|
||||||
flexDirection: "column",
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{userProfile.friends.map((friend) => {
|
{userProfile.libraryGames.map((game) => (
|
||||||
return (
|
<button
|
||||||
<button
|
key={game.objectID}
|
||||||
key={friend.id}
|
className={cn(
|
||||||
className={cn(
|
styles.gameListItem,
|
||||||
styles.profileContentBox,
|
styles.profileContentBox
|
||||||
styles.friendListContainer
|
)}
|
||||||
)}
|
onClick={() => handleGameClick(game)}
|
||||||
onClick={() => handleOnClickFriend(friend.id)}
|
title={game.title}
|
||||||
>
|
|
||||||
<div className={styles.friendAvatarContainer}>
|
|
||||||
{friend.profileImageUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.friendProfileIcon}
|
|
||||||
src={friend.profileImageUrl}
|
|
||||||
alt={friend.displayName}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={24} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={styles.friendListDisplayName}>
|
|
||||||
{friend.displayName}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{isMe && (
|
|
||||||
<Button
|
|
||||||
theme="outline"
|
|
||||||
onClick={() =>
|
|
||||||
showFriendsModal(
|
|
||||||
UserFriendModalTab.AddFriend,
|
|
||||||
userProfile.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<PlusIcon /> {t("add")}
|
{game.iconUrl ? (
|
||||||
</Button>
|
<img
|
||||||
)}
|
className={styles.libraryGameIcon}
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SteamLogo className={styles.libraryGameIcon} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{showFriends && (
|
||||||
|
<div className={styles.friendsSection}>
|
||||||
|
<button
|
||||||
|
className={styles.friendsSectionHeader}
|
||||||
|
onClick={() =>
|
||||||
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.FriendsList,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h2>{t("friends")}</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: vars.color.border,
|
||||||
|
height: "1px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
|
{userProfile.totalFriends}
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userProfile.friends.map((friend) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={friend.id}
|
||||||
|
className={cn(
|
||||||
|
styles.profileContentBox,
|
||||||
|
styles.friendListContainer
|
||||||
|
)}
|
||||||
|
onClick={() => handleOnClickFriend(friend.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{friend.profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.friendProfileIcon}
|
||||||
|
src={friend.profileImageUrl}
|
||||||
|
alt={friend.displayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.friendListDisplayName}>
|
||||||
|
{friend.displayName}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isMe && (
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() =>
|
||||||
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.AddFriend,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t("add")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,147 +0,0 @@
|
|||||||
import { Button, Modal, TextField } from "@renderer/components";
|
|
||||||
import { UserProfile } from "@types";
|
|
||||||
import * as styles from "./user.css";
|
|
||||||
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export interface UserEditProfileModalProps {
|
|
||||||
userProfile: UserProfile;
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
updateUserProfile: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserEditProfileModal = ({
|
|
||||||
userProfile,
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
updateUserProfile,
|
|
||||||
}: UserEditProfileModalProps) => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
const [displayName, setDisplayName] = useState("");
|
|
||||||
const [newImagePath, setNewImagePath] = useState<string | null>(null);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const { patchUser } = useUserDetails();
|
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDisplayName(userProfile.displayName);
|
|
||||||
}, [userProfile.displayName]);
|
|
||||||
|
|
||||||
const handleChangeProfileAvatar = async () => {
|
|
||||||
const { filePaths } = await window.electron.showOpenDialog({
|
|
||||||
properties: ["openFile"],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Image",
|
|
||||||
extensions: ["jpg", "jpeg", "png", "webp"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filePaths && filePaths.length > 0) {
|
|
||||||
const path = filePaths[0];
|
|
||||||
|
|
||||||
setNewImagePath(path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
|
||||||
event
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
patchUser(displayName, newImagePath)
|
|
||||||
.then(async () => {
|
|
||||||
await updateUserProfile();
|
|
||||||
showSuccessToast(t("saved_successfully"));
|
|
||||||
cleanFormAndClose();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showErrorToast(t("try_again"));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSaving(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetModal = () => {
|
|
||||||
setDisplayName(userProfile.displayName);
|
|
||||||
setNewImagePath(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanFormAndClose = () => {
|
|
||||||
resetModal();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
|
||||||
if (newImagePath) return `local:${newImagePath}`;
|
|
||||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
|
||||||
return null;
|
|
||||||
}, [newImagePath, userProfile.profileImageUrl]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
title={t("edit_profile")}
|
|
||||||
onClose={cleanFormAndClose}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSaveProfile}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
width: "350px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.profileAvatarEditContainer}
|
|
||||||
onClick={handleChangeProfileAvatar}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={userProfile.displayName}
|
|
||||||
src={avatarUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={96} />
|
|
||||||
)}
|
|
||||||
<div className={styles.editProfileImageBadge}>
|
|
||||||
<DeviceCameraIcon size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label={t("display_name")}
|
|
||||||
value={displayName}
|
|
||||||
required
|
|
||||||
minLength={3}
|
|
||||||
containerProps={{ style: { width: "100%" } }}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={isSaving}
|
|
||||||
style={{ alignSelf: "end" }}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{isSaving ? t("saving") : t("save")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./user-profile-settings-modal";
|
@ -0,0 +1,118 @@
|
|||||||
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
|
import { UserFriend } from "@types";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item";
|
||||||
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
|
const pageSize = 12;
|
||||||
|
|
||||||
|
export const UserEditProfileBlockList = () => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
const { showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
|
const [blocks, setBlocks] = useState<UserFriend[]>([]);
|
||||||
|
const listContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { unblockUser } = useUserDetails();
|
||||||
|
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (page > maxPage) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
window.electron
|
||||||
|
.getUserBlocks(pageSize, page * pageSize)
|
||||||
|
.then((newPage) => {
|
||||||
|
if (page === 0) {
|
||||||
|
setMaxPage(newPage.totalBlocks / pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlocks([...blocks, ...newPage.blocks]);
|
||||||
|
setPage(page + 1);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = listContainer.current?.scrollTop || 0;
|
||||||
|
const scrollHeight = listContainer.current?.scrollHeight || 0;
|
||||||
|
const clientHeight = listContainer.current?.clientHeight || 0;
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (scrollTop < maxScrollTop * 0.9 || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = listContainer.current;
|
||||||
|
container?.addEventListener("scroll", handleScroll);
|
||||||
|
return () => container?.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
const reloadList = () => {
|
||||||
|
setPage(0);
|
||||||
|
setMaxPage(0);
|
||||||
|
setBlocks([]);
|
||||||
|
loadNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUnblock = (userId: string) => {
|
||||||
|
unblockUser(userId)
|
||||||
|
.then(() => {
|
||||||
|
reloadList();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
|
<div
|
||||||
|
ref={listContainer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isLoading && blocks.length === 0 && <p>{t("no_blocked_users")}</p>}
|
||||||
|
{blocks.map((friend) => {
|
||||||
|
return (
|
||||||
|
<UserFriendItem
|
||||||
|
userId={friend.id}
|
||||||
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickUnblock={handleUnblock}
|
||||||
|
type={"BLOCKED"}
|
||||||
|
key={friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,149 @@
|
|||||||
|
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
|
import { Button, SelectField, TextField } from "@renderer/components";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import * as styles from "../user.css";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
|
export interface UserEditProfileProps {
|
||||||
|
userProfile: UserProfile;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserEditProfile = ({
|
||||||
|
userProfile,
|
||||||
|
updateUserProfile,
|
||||||
|
}: UserEditProfileProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
displayName: userProfile.displayName,
|
||||||
|
profileVisibility: userProfile.profileVisibility,
|
||||||
|
imageProfileUrl: null as string | null,
|
||||||
|
});
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const { patchUser } = useUserDetails();
|
||||||
|
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [profileVisibilityOptions, setProfileVisibilityOptions] = useState<
|
||||||
|
{ value: string; label: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProfileVisibilityOptions([
|
||||||
|
{ value: "PUBLIC", label: t("public") },
|
||||||
|
{ value: "FRIENDS", label: t("friends_only") },
|
||||||
|
{ value: "PRIVATE", label: t("private") },
|
||||||
|
]);
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const handleChangeProfileAvatar = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
extensions: ["jpg", "jpeg", "png", "webp"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const path = filePaths[0];
|
||||||
|
|
||||||
|
setForm({ ...form, imageProfileUrl: path });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileVisibilityChange = (event) => {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
profileVisibility: event.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
patchUser(form)
|
||||||
|
.then(async () => {
|
||||||
|
await updateUserProfile();
|
||||||
|
showSuccessToast(t("saved_successfully"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsSaving(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarUrl = useMemo(() => {
|
||||||
|
if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
|
||||||
|
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||||
|
return null;
|
||||||
|
}, [form, userProfile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSaveProfile}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
width: "350px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={96} />
|
||||||
|
)}
|
||||||
|
<div className={styles.editProfileImageBadge}>
|
||||||
|
<DeviceCameraIcon size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("display_name")}
|
||||||
|
value={form.displayName}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
label={t("privacy")}
|
||||||
|
value={form.profileVisibility}
|
||||||
|
onChange={handleProfileVisibilityChange}
|
||||||
|
options={profileVisibilityOptions.map((visiblity) => ({
|
||||||
|
key: visiblity.value,
|
||||||
|
value: visiblity.value,
|
||||||
|
label: visiblity.label,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
||||||
|
{isSaving ? t("saving") : t("save")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,73 @@
|
|||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserEditProfile } from "./user-edit-profile";
|
||||||
|
import { UserEditProfileBlockList } from "./user-block-list";
|
||||||
|
|
||||||
|
export interface UserProfileSettingsModalProps {
|
||||||
|
userProfile: UserProfile;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfileSettingsModal = ({
|
||||||
|
userProfile,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
updateUserProfile,
|
||||||
|
}: UserProfileSettingsModalProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const tabs = [t("edit_profile"), t("blocked_users")];
|
||||||
|
|
||||||
|
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||||
|
|
||||||
|
const renderTab = () => {
|
||||||
|
if (currentTabIndex == 0) {
|
||||||
|
return (
|
||||||
|
<UserEditProfile
|
||||||
|
userProfile={userProfile}
|
||||||
|
updateUserProfile={updateUserProfile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTabIndex == 1) {
|
||||||
|
return <UserEditProfileBlockList />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal visible={visible} title={t("settings")} onClose={onClose}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
theme={index === currentTabIndex ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentTabIndex(index)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
{renderTab()}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -60,6 +60,7 @@ export const friendListDisplayName = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const profileAvatarEditContainer = style({
|
export const profileAvatarEditContainer = style({
|
||||||
|
alignSelf: "center",
|
||||||
width: "128px",
|
width: "128px",
|
||||||
height: "128px",
|
height: "128px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -31,7 +31,7 @@ export const User = () => {
|
|||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [dispatch, userId, t]);
|
}, [dispatch, navigate, showErrorToast, userId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
|
@ -3,6 +3,7 @@ export enum Downloader {
|
|||||||
Torrent,
|
Torrent,
|
||||||
Gofile,
|
Gofile,
|
||||||
PixelDrain,
|
PixelDrain,
|
||||||
|
Qiwi,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadSourceStatus {
|
export enum DownloadSourceStatus {
|
||||||
@ -73,13 +74,27 @@ const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
|
|||||||
|
|
||||||
export const getDownloadersForUri = (uri: string) => {
|
export const getDownloadersForUri = (uri: string) => {
|
||||||
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
|
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
|
||||||
|
|
||||||
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
|
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
|
||||||
|
if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
|
||||||
|
|
||||||
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
||||||
return [Downloader.RealDebrid];
|
return [Downloader.RealDebrid];
|
||||||
|
|
||||||
if (uri.startsWith("magnet:"))
|
if (uri.startsWith("magnet:")) {
|
||||||
return [Downloader.Torrent, Downloader.RealDebrid];
|
return [Downloader.Torrent, Downloader.RealDebrid];
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDownloadersForUris = (uris: string[]) => {
|
||||||
|
const downloadersSet = uris.reduce<Set<Downloader>>((prev, next) => {
|
||||||
|
const downloaders = getDownloadersForUri(next);
|
||||||
|
downloaders.forEach((downloader) => prev.add(downloader));
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
return Array.from(downloadersSet);
|
||||||
|
};
|
||||||
|
@ -67,7 +67,11 @@ export interface SteamAppDetails {
|
|||||||
export interface GameRepack {
|
export interface GameRepack {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* @deprecated Use uris instead
|
||||||
|
*/
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
uris: string[];
|
||||||
repacker: string;
|
repacker: string;
|
||||||
fileSize: string | null;
|
fileSize: string | null;
|
||||||
uploadDate: Date | string | null;
|
uploadDate: Date | string | null;
|
||||||
@ -137,9 +141,9 @@ export interface Game {
|
|||||||
export type LibraryGame = Omit<Game, "repacks">;
|
export type LibraryGame = Omit<Game, "repacks">;
|
||||||
|
|
||||||
export interface GameRunning {
|
export interface GameRunning {
|
||||||
id: number;
|
id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
iconUrl: string;
|
iconUrl: string | null;
|
||||||
objectID: string;
|
objectID: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
sessionDurationInMillis: number;
|
sessionDurationInMillis: number;
|
||||||
@ -194,6 +198,7 @@ export interface StartGameDownloadPayload {
|
|||||||
objectID: string;
|
objectID: string;
|
||||||
title: string;
|
title: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
uri: string;
|
||||||
downloadPath: string;
|
downloadPath: string;
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
}
|
}
|
||||||
@ -282,6 +287,11 @@ export interface UserFriends {
|
|||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserBlocks {
|
||||||
|
totalBlocks: number;
|
||||||
|
blocks: UserFriend[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -308,6 +318,14 @@ export interface UserProfile {
|
|||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
totalFriends: number;
|
totalFriends: number;
|
||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
|
currentGame: GameRunning | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfileProps {
|
||||||
|
displayName?: string;
|
||||||
|
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
profileImageUrl?: string | null;
|
||||||
|
bio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
|
@ -20,6 +20,23 @@ if start_download_payload:
|
|||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
rpc_password_header = 'x-hydra-rpc-password'
|
rpc_password_header = 'x-hydra-rpc-password'
|
||||||
|
|
||||||
|
skip_log_routes = [
|
||||||
|
"process-list",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
|
||||||
|
def log_error(self, format, *args):
|
||||||
|
sys.stderr.write("%s - - [%s] %s\n" %
|
||||||
|
(self.address_string(),
|
||||||
|
self.log_date_time_string(),
|
||||||
|
format%args))
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
for route in self.skip_log_routes:
|
||||||
|
if route in args[0]: return
|
||||||
|
|
||||||
|
super().log_message(format, *args)
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/status":
|
if self.path == "/status":
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
|
Loading…
Reference in New Issue
Block a user