mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-29 05:41:03 +00:00
Compare commits
20 Commits
fix/fullsc
...
feat/wrapp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35736dd2d9 | ||
|
|
86d5547aa1 | ||
|
|
358f41b4ba | ||
|
|
7f0dc5eee4 | ||
|
|
067f7a00be | ||
|
|
37f085e2c0 | ||
|
|
f8ac284bc2 | ||
|
|
b1d72828bb | ||
|
|
4c09f915c6 | ||
|
|
f77b2116c1 | ||
|
|
ccb754fa13 | ||
|
|
5329cc446f | ||
|
|
21a0ad1500 | ||
|
|
9ffaee12d1 | ||
|
|
8555274589 | ||
|
|
a152c89d7f | ||
|
|
879f0baad7 | ||
|
|
c025dc199d | ||
|
|
5b4b258526 | ||
|
|
0268829946 |
@@ -723,7 +723,10 @@
|
|||||||
"karma_description": "Earned from positive likes on reviews",
|
"karma_description": "Earned from positive likes on reviews",
|
||||||
"user_reviews": "Reviews",
|
"user_reviews": "Reviews",
|
||||||
"delete_review": "Delete Review",
|
"delete_review": "Delete Review",
|
||||||
"loading_reviews": "Loading reviews..."
|
"loading_reviews": "Loading reviews...",
|
||||||
|
"wrapped_2025": "Wrapped 2025",
|
||||||
|
"view_my_wrapped_button": "View My Wrapped 2025",
|
||||||
|
"view_wrapped_button": "View {{displayName}}'s Wrapped 2025"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const startGameDownload = async (
|
|||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
/* Delete any previous download */
|
|
||||||
await downloadsSublevel.del(gameKey);
|
await downloadsSublevel.del(gameKey);
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
@@ -124,6 +123,42 @@ const startGameDownload = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
if (downloader === Downloader.Buzzheavier) {
|
||||||
|
if (err.message.includes("Rate limit")) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Buzzheavier: Rate limit exceeded",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
err.message.includes("not found") ||
|
||||||
|
err.message.includes("deleted")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Buzzheavier: File not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloader === Downloader.FuckingFast) {
|
||||||
|
if (err.message.includes("Rate limit")) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "FuckingFast: Rate limit exceeded",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
err.message.includes("not found") ||
|
||||||
|
err.message.includes("deleted")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "FuckingFast: File not found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { ok: false, error: err.message };
|
return { ok: false, error: err.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,59 @@ import { RealDebridClient } from "./real-debrid";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { orderBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import { TorBoxClient } from "./torbox";
|
import { TorBoxClient } from "./torbox";
|
||||||
import { GameFilesManager } from "../game-files-manager";
|
import { GameFilesManager } from "../game-files-manager";
|
||||||
import { HydraDebridClient } from "./hydra-debrid";
|
import { HydraDebridClient } from "./hydra-debrid";
|
||||||
|
import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static downloadingGameId: string | null = null;
|
private static downloadingGameId: string | null = null;
|
||||||
|
|
||||||
|
private static extractFilename(url: string, originalUrl?: string): string | undefined {
|
||||||
|
if (originalUrl?.includes('#')) {
|
||||||
|
const hashPart = originalUrl.split('#')[1];
|
||||||
|
if (hashPart && !hashPart.startsWith('http')) return hashPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes('#')) {
|
||||||
|
const hashPart = url.split('#')[1];
|
||||||
|
if (hashPart && !hashPart.startsWith('http')) return hashPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const filename = urlObj.pathname.split('/').pop();
|
||||||
|
if (filename?.length) return filename;
|
||||||
|
} catch {
|
||||||
|
// Invalid URL
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static sanitizeFilename(filename: string): string {
|
||||||
|
return filename.replace(/[<>:"/\\|?*]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createDownloadPayload(directUrl: string, originalUrl: string, downloadId: string, savePath: string) {
|
||||||
|
const filename = this.extractFilename(directUrl, originalUrl);
|
||||||
|
const sanitizedFilename = filename ? this.sanitizeFilename(filename) : undefined;
|
||||||
|
|
||||||
|
if (sanitizedFilename) {
|
||||||
|
logger.log(`[DownloadManager] Using filename: ${sanitizedFilename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "start" as const,
|
||||||
|
game_id: downloadId,
|
||||||
|
url: directUrl,
|
||||||
|
save_path: savePath,
|
||||||
|
out: sanitizedFilename,
|
||||||
|
allow_multiple_connections: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static async startRPC(
|
public static async startRPC(
|
||||||
download?: Download,
|
download?: Download,
|
||||||
downloadsToSeed?: Download[]
|
downloadsToSeed?: Download[]
|
||||||
@@ -53,9 +98,7 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async getDownloadStatus() {
|
private static async getDownloadStatus() {
|
||||||
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>("/status");
|
||||||
"/status"
|
|
||||||
);
|
|
||||||
if (response.data === null || !this.downloadingGameId) return null;
|
if (response.data === null || !this.downloadingGameId) return null;
|
||||||
const downloadId = this.downloadingGameId;
|
const downloadId = this.downloadingGameId;
|
||||||
|
|
||||||
@@ -71,8 +114,7 @@ export class DownloadManager {
|
|||||||
status,
|
status,
|
||||||
} = response.data;
|
} = response.data;
|
||||||
|
|
||||||
const isDownloadingMetadata =
|
const isDownloadingMetadata = status === LibtorrentStatus.DownloadingMetadata;
|
||||||
status === LibtorrentStatus.DownloadingMetadata;
|
|
||||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||||
|
|
||||||
const download = await downloadsSublevel.get(downloadId);
|
const download = await downloadsSublevel.get(downloadId);
|
||||||
@@ -121,29 +163,29 @@ export class DownloadManager {
|
|||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{ valueEncoding: "json" }
|
||||||
valueEncoding: "json",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (WindowManager.mainWindow && download) {
|
||||||
|
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||||
|
WindowManager.mainWindow.webContents.send(
|
||||||
|
"on-download-progress",
|
||||||
|
JSON.parse(JSON.stringify({ ...status, game }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const shouldExtract = download.automaticallyExtract;
|
const shouldExtract = download.automaticallyExtract;
|
||||||
|
|
||||||
// Handle download completion BEFORE sending progress to renderer
|
|
||||||
// This ensures extraction starts and DB is updated before UI reacts
|
|
||||||
if (progress === 1 && download) {
|
if (progress === 1 && download) {
|
||||||
publishDownloadCompleteNotification(game);
|
publishDownloadCompleteNotification(game);
|
||||||
|
|
||||||
if (
|
if (userPreferences?.seedAfterDownloadComplete && download.downloader === Downloader.Torrent) {
|
||||||
userPreferences?.seedAfterDownloadComplete &&
|
|
||||||
download.downloader === Downloader.Torrent
|
|
||||||
) {
|
|
||||||
await downloadsSublevel.put(gameId, {
|
await downloadsSublevel.put(gameId, {
|
||||||
...download,
|
...download,
|
||||||
status: "seeding",
|
status: "seeding",
|
||||||
shouldSeed: true,
|
shouldSeed: true,
|
||||||
queued: false,
|
queued: false,
|
||||||
extracting: shouldExtract,
|
extracting: shouldExtract,
|
||||||
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await downloadsSublevel.put(gameId, {
|
await downloadsSublevel.put(gameId, {
|
||||||
@@ -152,54 +194,31 @@ export class DownloadManager {
|
|||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
queued: false,
|
queued: false,
|
||||||
extracting: shouldExtract,
|
extracting: shouldExtract,
|
||||||
extractionProgress: shouldExtract ? 0 : download.extractionProgress,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cancelDownload(gameId);
|
this.cancelDownload(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldExtract) {
|
if (shouldExtract) {
|
||||||
// Send initial extraction progress BEFORE download progress
|
const gameFilesManager = new GameFilesManager(game.shop, game.objectId);
|
||||||
// This ensures the UI shows extraction immediately
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
"on-extraction-progress",
|
|
||||||
game.shop,
|
|
||||||
game.objectId,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const gameFilesManager = new GameFilesManager(
|
if (FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))) {
|
||||||
game.shop,
|
|
||||||
game.objectId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
|
|
||||||
download.folderName?.endsWith(ext)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
gameFilesManager.extractDownloadedFile();
|
gameFilesManager.extractDownloadedFile();
|
||||||
} else {
|
} else {
|
||||||
gameFilesManager
|
gameFilesManager
|
||||||
.extractFilesInDirectory(
|
.extractFilesInDirectory(path.join(download.downloadPath, download.folderName!))
|
||||||
path.join(download.downloadPath, download.folderName!)
|
.then(() => gameFilesManager.setExtractionComplete());
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
gameFilesManager.setExtractionComplete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloads = await downloadsSublevel
|
const downloads = await downloadsSublevel
|
||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => sortBy(
|
||||||
return orderBy(
|
games.filter((game) => game.status === "paused" && game.queued),
|
||||||
games.filter((game) => game.status === "paused" && game.queued),
|
"timestamp",
|
||||||
"timestamp",
|
"DESC"
|
||||||
"desc"
|
));
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [nextItemOnQueue] = downloads;
|
const [nextItemOnQueue] = downloads;
|
||||||
|
|
||||||
@@ -209,18 +228,6 @@ export class DownloadManager {
|
|||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send progress to renderer after completion handling
|
|
||||||
if (WindowManager.mainWindow && download) {
|
|
||||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
|
||||||
WindowManager.mainWindow.webContents.send(
|
|
||||||
"on-download-progress",
|
|
||||||
structuredClone({
|
|
||||||
...status,
|
|
||||||
game,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,9 +245,7 @@ export class DownloadManager {
|
|||||||
|
|
||||||
if (!download) return;
|
if (!download) return;
|
||||||
|
|
||||||
const totalSize = await getDirSize(
|
const totalSize = await getDirSize(path.join(download.downloadPath, status.folderName));
|
||||||
path.join(download.downloadPath, status.folderName)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (totalSize < status.fileSize) {
|
if (totalSize < status.fileSize) {
|
||||||
await this.cancelDownload(status.gameId);
|
await this.cancelDownload(status.gameId);
|
||||||
@@ -261,10 +266,7 @@ export class DownloadManager {
|
|||||||
|
|
||||||
static async pauseDownload(downloadKey = this.downloadingGameId) {
|
static async pauseDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc
|
await PythonRPC.rpc
|
||||||
.post("/action", {
|
.post("/action", { action: "pause", game_id: downloadKey } as PauseDownloadPayload)
|
||||||
action: "pause",
|
|
||||||
game_id: downloadKey,
|
|
||||||
} as PauseDownloadPayload)
|
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
@@ -279,13 +281,8 @@ export class DownloadManager {
|
|||||||
|
|
||||||
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc
|
await PythonRPC.rpc
|
||||||
.post("/action", {
|
.post("/action", { action: "cancel", game_id: downloadKey })
|
||||||
action: "cancel",
|
.catch((err) => logger.error("Failed to cancel game download", err));
|
||||||
game_id: downloadKey,
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Failed to cancel game download", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (downloadKey === this.downloadingGameId) {
|
if (downloadKey === this.downloadingGameId) {
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@@ -318,7 +315,6 @@ export class DownloadManager {
|
|||||||
const id = download.uri.split("/").pop();
|
const id = download.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!);
|
||||||
|
|
||||||
await GofileApi.checkDownloadUrl(downloadLink);
|
await GofileApi.checkDownloadUrl(downloadLink);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -360,9 +356,30 @@ export class DownloadManager {
|
|||||||
save_path: download.downloadPath,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Downloader.Buzzheavier: {
|
||||||
|
logger.log(`[DownloadManager] Processing Buzzheavier download for URI: ${download.uri}`);
|
||||||
|
try {
|
||||||
|
const directUrl = await BuzzheavierApi.getDirectLink(download.uri);
|
||||||
|
logger.log(`[DownloadManager] Buzzheavier direct URL obtained`);
|
||||||
|
return this.createDownloadPayload(directUrl, download.uri, downloadId, download.downloadPath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[DownloadManager] Error processing Buzzheavier download:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Downloader.FuckingFast: {
|
||||||
|
logger.log(`[DownloadManager] Processing FuckingFast download for URI: ${download.uri}`);
|
||||||
|
try {
|
||||||
|
const directUrl = await FuckingFastApi.getDirectLink(download.uri);
|
||||||
|
logger.log(`[DownloadManager] FuckingFast direct URL obtained`);
|
||||||
|
return this.createDownloadPayload(directUrl, download.uri, downloadId, download.downloadPath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[DownloadManager] Error processing FuckingFast download:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
case Downloader.Mediafire: {
|
case Downloader.Mediafire: {
|
||||||
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
|
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
@@ -379,7 +396,6 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
case Downloader.RealDebrid: {
|
case Downloader.RealDebrid: {
|
||||||
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
|
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -392,7 +408,6 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
case Downloader.TorBox: {
|
case Downloader.TorBox: {
|
||||||
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
|
||||||
|
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
@@ -404,10 +419,7 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.Hydra: {
|
case Downloader.Hydra: {
|
||||||
const downloadUrl = await HydraDebridClient.getDownloadUrl(
|
const downloadUrl = await HydraDebridClient.getDownloadUrl(download.uri);
|
||||||
download.uri
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
|
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
82
src/main/services/hosters/buzzheavier.ts
Normal file
82
src/main/services/hosters/buzzheavier.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {
|
||||||
|
HOSTER_USER_AGENT,
|
||||||
|
extractHosterFilename,
|
||||||
|
handleHosterError,
|
||||||
|
} from "./fuckingfast";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
export class BuzzheavierApi {
|
||||||
|
private static readonly BUZZHEAVIER_DOMAINS = [
|
||||||
|
"buzzheavier.com",
|
||||||
|
"bzzhr.co",
|
||||||
|
"fuckingfast.net",
|
||||||
|
];
|
||||||
|
|
||||||
|
private static isSupportedDomain(url: string): boolean {
|
||||||
|
const lowerUrl = url.toLowerCase();
|
||||||
|
return this.BUZZHEAVIER_DOMAINS.some((domain) => lowerUrl.includes(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getBuzzheavierDirectLink(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const baseUrl = url.split("#")[0];
|
||||||
|
logger.log(`[Buzzheavier] Starting download link extraction for: ${baseUrl}`);
|
||||||
|
|
||||||
|
await axios.get(baseUrl, {
|
||||||
|
headers: { "User-Agent": HOSTER_USER_AGENT },
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadUrl = `${baseUrl}/download`;
|
||||||
|
logger.log(`[Buzzheavier] Making HEAD request to: ${downloadUrl}`);
|
||||||
|
const headResponse = await axios.head(downloadUrl, {
|
||||||
|
headers: {
|
||||||
|
"hx-current-url": baseUrl,
|
||||||
|
"hx-request": "true",
|
||||||
|
referer: baseUrl,
|
||||||
|
"User-Agent": HOSTER_USER_AGENT,
|
||||||
|
},
|
||||||
|
maxRedirects: 0,
|
||||||
|
validateStatus: (status) =>
|
||||||
|
status === 200 || status === 204 || status === 301 || status === 302,
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hxRedirect = headResponse.headers["hx-redirect"];
|
||||||
|
logger.log(`[Buzzheavier] Received hx-redirect header: ${hxRedirect}`);
|
||||||
|
if (!hxRedirect) {
|
||||||
|
logger.error(`[Buzzheavier] No hx-redirect header found. Status: ${headResponse.status}`);
|
||||||
|
throw new Error(
|
||||||
|
"Could not extract download link. File may be deleted or is a directory."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = new URL(baseUrl).hostname;
|
||||||
|
const directLink = hxRedirect.startsWith("/dl/")
|
||||||
|
? `https://${domain}${hxRedirect}`
|
||||||
|
: hxRedirect;
|
||||||
|
logger.log(`[Buzzheavier] Extracted direct link`);
|
||||||
|
return directLink;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[Buzzheavier] Error in getBuzzheavierDirectLink:`, error);
|
||||||
|
handleHosterError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDirectLink(url: string): Promise<string> {
|
||||||
|
if (!this.isSupportedDomain(url)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported domain. Supported domains: ${this.BUZZHEAVIER_DOMAINS.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.getBuzzheavierDirectLink(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getFilename(
|
||||||
|
url: string,
|
||||||
|
directUrl?: string
|
||||||
|
): Promise<string> {
|
||||||
|
return extractHosterFilename(url, directUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/main/services/hosters/fuckingfast.ts
Normal file
129
src/main/services/hosters/fuckingfast.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
export const HOSTER_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0";
|
||||||
|
|
||||||
|
export async function extractHosterFilename(
|
||||||
|
url: string,
|
||||||
|
directUrl?: string
|
||||||
|
): Promise<string> {
|
||||||
|
if (url.includes("#")) {
|
||||||
|
const fragment = url.split("#")[1];
|
||||||
|
if (fragment && !fragment.startsWith("http")) {
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directUrl) {
|
||||||
|
try {
|
||||||
|
const response = await axios.head(directUrl, {
|
||||||
|
timeout: 10000,
|
||||||
|
headers: { "User-Agent": HOSTER_USER_AGENT },
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDisposition = response.headers["content-disposition"];
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(
|
||||||
|
contentDisposition
|
||||||
|
);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
return filenameMatch[1].replace(/['"]/g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlPath = new URL(directUrl).pathname;
|
||||||
|
const filename = urlPath.split("/").pop()?.split("?")[0];
|
||||||
|
if (filename) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "downloaded_file";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleHosterError(error: unknown): never {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("File not found");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
throw new Error("Rate limit exceeded. Please try again later.");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 403) {
|
||||||
|
throw new Error("Access denied. File may be private or deleted.");
|
||||||
|
}
|
||||||
|
throw new Error(`Network error: ${error.response?.status || "Unknown"}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// FuckingFast API Class
|
||||||
|
// ============================================
|
||||||
|
export class FuckingFastApi {
|
||||||
|
private static readonly FUCKINGFAST_DOMAINS = ["fuckingfast.co"];
|
||||||
|
|
||||||
|
private static readonly FUCKINGFAST_REGEX =
|
||||||
|
/window\.open\("(https:\/\/fuckingfast\.co\/dl\/[^"]*)"\)/;
|
||||||
|
|
||||||
|
private static isSupportedDomain(url: string): boolean {
|
||||||
|
const lowerUrl = url.toLowerCase();
|
||||||
|
return this.FUCKINGFAST_DOMAINS.some((domain) => lowerUrl.includes(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getFuckingFastDirectLink(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
logger.log(`[FuckingFast] Starting download link extraction for: ${url}`);
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: { "User-Agent": HOSTER_USER_AGENT },
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
if (html.toLowerCase().includes("rate limit")) {
|
||||||
|
logger.error(`[FuckingFast] Rate limit detected`);
|
||||||
|
throw new Error(
|
||||||
|
"Rate limit exceeded. Please wait a few minutes and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html.includes("File Not Found Or Deleted")) {
|
||||||
|
logger.error(`[FuckingFast] File not found or deleted`);
|
||||||
|
throw new Error("File not found or deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = this.FUCKINGFAST_REGEX.exec(html);
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
logger.error(`[FuckingFast] Could not extract download link`);
|
||||||
|
throw new Error("Could not extract download link from page");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`[FuckingFast] Successfully extracted direct link`);
|
||||||
|
return match[1];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[FuckingFast] Error:`, error);
|
||||||
|
handleHosterError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDirectLink(url: string): Promise<string> {
|
||||||
|
if (!this.isSupportedDomain(url)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported domain. Supported domains: ${this.FUCKINGFAST_DOMAINS.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.getFuckingFastDirectLink(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getFilename(
|
||||||
|
url: string,
|
||||||
|
directUrl?: string
|
||||||
|
): Promise<string> {
|
||||||
|
return extractHosterFilename(url, directUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,3 +3,5 @@ export * from "./qiwi";
|
|||||||
export * from "./datanodes";
|
export * from "./datanodes";
|
||||||
export * from "./mediafire";
|
export * from "./mediafire";
|
||||||
export * from "./pixeldrain";
|
export * from "./pixeldrain";
|
||||||
|
export * from "./buzzheavier";
|
||||||
|
export * from "./fuckingfast";
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ export class WindowManager {
|
|||||||
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
|
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
|
||||||
{
|
{
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 720,
|
height: 860,
|
||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 540,
|
minHeight: 860,
|
||||||
backgroundColor: "#1c1c1c",
|
backgroundColor: "#1c1c1c",
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
icon,
|
icon,
|
||||||
@@ -106,7 +106,7 @@ export class WindowManager {
|
|||||||
valueEncoding: "json",
|
valueEncoding: "json",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data ?? { isMaximized: false, height: 720, width: 1200 };
|
return data ?? { isMaximized: false, height: 860, width: 1200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static updateInitialConfig(
|
private static updateInitialConfig(
|
||||||
@@ -224,7 +224,7 @@ export class WindowManager {
|
|||||||
? {
|
? {
|
||||||
x: undefined,
|
x: undefined,
|
||||||
y: undefined,
|
y: undefined,
|
||||||
height: this.initialConfigInitializationMainWindow.height ?? 720,
|
height: this.initialConfigInitializationMainWindow.height ?? 860,
|
||||||
width: this.initialConfigInitializationMainWindow.width ?? 1200,
|
width: this.initialConfigInitializationMainWindow.width ?? 1200,
|
||||||
isMaximized: true,
|
isMaximized: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const DOWNLOADER_NAME = {
|
|||||||
[Downloader.Qiwi]: "Qiwi",
|
[Downloader.Qiwi]: "Qiwi",
|
||||||
[Downloader.Datanodes]: "Datanodes",
|
[Downloader.Datanodes]: "Datanodes",
|
||||||
[Downloader.Mediafire]: "Mediafire",
|
[Downloader.Mediafire]: "Mediafire",
|
||||||
|
[Downloader.Buzzheavier]: "Buzzheavier",
|
||||||
|
[Downloader.FuckingFast]: "FuckingFast",
|
||||||
[Downloader.TorBox]: "TorBox",
|
[Downloader.TorBox]: "TorBox",
|
||||||
[Downloader.Hydra]: "Nimbus",
|
[Downloader.Hydra]: "Nimbus",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { UserKarmaBox } from "./user-karma-box";
|
|||||||
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
|
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
|
||||||
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
|
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import { ProfileTabs } from "./profile-tabs";
|
import { ProfileTabs, type ProfileTabType } from "./profile-tabs";
|
||||||
import { LibraryTab } from "./library-tab";
|
import { LibraryTab } from "./library-tab";
|
||||||
import { ReviewsTab } from "./reviews-tab";
|
import { ReviewsTab } from "./reviews-tab";
|
||||||
import { AnimatePresence } from "framer-motion";
|
import { AnimatePresence } from "framer-motion";
|
||||||
@@ -95,7 +95,7 @@ export function ProfileContent() {
|
|||||||
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
||||||
const statsAnimation = useRef(-1);
|
const statsAnimation = useRef(-1);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<"library" | "reviews">("library");
|
const [activeTab, setActiveTab] = useState<ProfileTabType>("library");
|
||||||
|
|
||||||
// User reviews state
|
// User reviews state
|
||||||
const [reviews, setReviews] = useState<UserReview[]>([]);
|
const [reviews, setReviews] = useState<UserReview[]>([]);
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { motion } from "framer-motion";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import "./profile-content.scss";
|
import "./profile-content.scss";
|
||||||
|
|
||||||
|
export type ProfileTabType = "library" | "reviews";
|
||||||
|
|
||||||
interface ProfileTabsProps {
|
interface ProfileTabsProps {
|
||||||
activeTab: "library" | "reviews";
|
activeTab: ProfileTabType;
|
||||||
reviewsTotalCount: number;
|
reviewsTotalCount: number;
|
||||||
onTabChange: (tab: "library" | "reviews") => void;
|
onTabChange: (tab: ProfileTabType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileTabs({
|
export function ProfileTabs({
|
||||||
|
|||||||
100
src/renderer/src/pages/profile/profile-content/wrapped-tab.scss
Normal file
100
src/renderer/src/pages/profile/profile-content/wrapped-tab.scss
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.wrapped-fullscreen-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(globals.$spacing-unit * 5);
|
||||||
|
right: calc(globals.$spacing-unit * 5);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loader {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: wrapped-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wrapped-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { XIcon } from "@primer/octicons-react";
|
||||||
|
import "./wrapped-tab.scss";
|
||||||
|
|
||||||
|
interface WrappedFullscreenModalProps {
|
||||||
|
userId: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScaleConfig {
|
||||||
|
scale: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCALE_CONFIGS: Record<number, ScaleConfig> = {
|
||||||
|
0.25: { scale: 0.25, width: 270, height: 480 },
|
||||||
|
0.3: { scale: 0.3, width: 324, height: 576 },
|
||||||
|
0.5: { scale: 0.5, width: 540, height: 960 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScaleConfigForHeight = (height: number): ScaleConfig => {
|
||||||
|
if (height >= 1000) return SCALE_CONFIGS[0.5];
|
||||||
|
if (height >= 650) return SCALE_CONFIGS[0.3];
|
||||||
|
return SCALE_CONFIGS[0.25];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WrappedFullscreenModal({
|
||||||
|
userId,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: Readonly<WrappedFullscreenModalProps>) {
|
||||||
|
const [config, setConfig] = useState<ScaleConfig>(SCALE_CONFIGS[0.5]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const updateConfig = () => {
|
||||||
|
setConfig(getScaleConfigForHeight(window.innerHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConfig();
|
||||||
|
window.addEventListener("resize", updateConfig);
|
||||||
|
return () => window.removeEventListener("resize", updateConfig);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog className="wrapped-fullscreen-modal" aria-modal="true" open>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="wrapped-fullscreen-modal__backdrop"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close wrapped"
|
||||||
|
/>
|
||||||
|
<div className="wrapped-fullscreen-modal__container">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="wrapped-fullscreen-modal__close-button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close wrapped"
|
||||||
|
>
|
||||||
|
<XIcon size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="wrapped-fullscreen-modal__content"
|
||||||
|
style={{ width: config.width, height: config.height }}
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="wrapped-fullscreen-modal__loader">
|
||||||
|
<div className="wrapped-fullscreen-modal__spinner" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<iframe
|
||||||
|
src={`https://hydrawrapped.com/embed/${userId}?scale=${config.scale}`}
|
||||||
|
className="wrapped-fullscreen-modal__iframe"
|
||||||
|
title="Wrapped 2025"
|
||||||
|
onLoad={() => setIsLoading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -120,6 +120,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__left-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
@@ -131,5 +136,35 @@
|
|||||||
&--outline {
|
&--outline {
|
||||||
border-color: globals.$body-color;
|
border-color: globals.$body-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--wrapped {
|
||||||
|
background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
#2a57ff 0%,
|
||||||
|
#2951e6 11%,
|
||||||
|
#2f5bff 16%,
|
||||||
|
#2c56e8 29%,
|
||||||
|
#244acc 34%,
|
||||||
|
#2245c2 40%,
|
||||||
|
#3a6bff 45%,
|
||||||
|
#3766f2 50%,
|
||||||
|
#2444b8 56%,
|
||||||
|
#122a73 82%,
|
||||||
|
#2348b3 86%,
|
||||||
|
#1f429e 87%,
|
||||||
|
#10286a 93%,
|
||||||
|
#0e2a63 100%
|
||||||
|
);
|
||||||
|
background-color: #2a57ff;
|
||||||
|
background-size: 105% 100%;
|
||||||
|
background-position: 100% 50%;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
transition: background-position 0.4s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
PersonAddIcon,
|
PersonAddIcon,
|
||||||
SignOutIcon,
|
SignOutIcon,
|
||||||
|
TrophyIcon,
|
||||||
XCircleFillIcon,
|
XCircleFillIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
@@ -27,6 +28,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import type { FriendRequestAction } from "@types";
|
import type { FriendRequestAction } from "@types";
|
||||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
|
import { WrappedFullscreenModal } from "../profile-content/wrapped-tab";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
@@ -38,6 +40,7 @@ type FriendAction =
|
|||||||
|
|
||||||
export function ProfileHero() {
|
export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
|
const [showWrappedModal, setShowWrappedModal] = useState(false);
|
||||||
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
|
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
|
||||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||||
|
|
||||||
@@ -280,6 +283,13 @@ export function ProfileHero() {
|
|||||||
onClose={() => setShowEditProfileModal(false)}
|
onClose={() => setShowEditProfileModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{userProfile && (
|
||||||
|
<WrappedFullscreenModal
|
||||||
|
userId={userProfile.id}
|
||||||
|
isOpen={showWrappedModal}
|
||||||
|
onClose={() => setShowWrappedModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FullscreenMediaModal
|
<FullscreenMediaModal
|
||||||
visible={showFullscreenAvatar}
|
visible={showFullscreenAvatar}
|
||||||
onClose={() => setShowFullscreenAvatar(false)}
|
onClose={() => setShowFullscreenAvatar(false)}
|
||||||
@@ -393,6 +403,22 @@ export function ProfileHero() {
|
|||||||
background: !backgroundImage ? heroBackground : undefined,
|
background: !backgroundImage ? heroBackground : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{userProfile?.hasCompletedWrapped2025 && (
|
||||||
|
<div className="profile-hero__left-actions">
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => setShowWrappedModal(true)}
|
||||||
|
className="profile-hero__button--wrapped"
|
||||||
|
>
|
||||||
|
<TrophyIcon />
|
||||||
|
{isMe
|
||||||
|
? t("view_my_wrapped_button")
|
||||||
|
: t("view_wrapped_button", {
|
||||||
|
displayName: userProfile.displayName,
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="profile-hero__actions">{profileActions}</div>
|
<div className="profile-hero__actions">{profileActions}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export enum Downloader {
|
|||||||
Mediafire,
|
Mediafire,
|
||||||
TorBox,
|
TorBox,
|
||||||
Hydra,
|
Hydra,
|
||||||
|
Buzzheavier,
|
||||||
|
FuckingFast,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadSourceStatus {
|
export enum DownloadSourceStatus {
|
||||||
|
|||||||
@@ -114,6 +114,16 @@ export const getDownloadersForUri = (uri: string) => {
|
|||||||
if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes];
|
if (uri.startsWith("https://datanodes.to")) return [Downloader.Datanodes];
|
||||||
if (uri.startsWith("https://www.mediafire.com"))
|
if (uri.startsWith("https://www.mediafire.com"))
|
||||||
return [Downloader.Mediafire];
|
return [Downloader.Mediafire];
|
||||||
|
if (
|
||||||
|
uri.startsWith("https://buzzheavier.com") ||
|
||||||
|
uri.startsWith("https://bzzhr.co") ||
|
||||||
|
uri.startsWith("https://fuckingfast.net")
|
||||||
|
) {
|
||||||
|
return [Downloader.Buzzheavier];
|
||||||
|
}
|
||||||
|
if (uri.startsWith("https://fuckingfast.co")) {
|
||||||
|
return [Downloader.FuckingFast];
|
||||||
|
}
|
||||||
|
|
||||||
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
||||||
return [Downloader.RealDebrid];
|
return [Downloader.RealDebrid];
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export interface UserProfile {
|
|||||||
backupsPerGameLimit: number;
|
backupsPerGameLimit: number;
|
||||||
};
|
};
|
||||||
badges: string[];
|
badges: string[];
|
||||||
|
hasCompletedWrapped2025: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user