mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: fixing download error for gofile
This commit is contained in:
parent
702b141f7b
commit
b122489b34
@ -125,7 +125,8 @@
|
||||
"refuse_nsfw_content": "Go back",
|
||||
"stats": "Stats",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Active players"
|
||||
"player_count": "Active players",
|
||||
"download_error": "This download option is not available"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
@ -207,7 +208,10 @@
|
||||
"friends_only": "Friends only",
|
||||
"privacy": "Privacy",
|
||||
"profile_visibility": "Profile visibility",
|
||||
"profile_visibility_description": "Choose who can see your profile and library"
|
||||
"profile_visibility_description": "Choose who can see your profile and library",
|
||||
"required_field": "This field is required",
|
||||
"source_already_exists": "This source has been already added",
|
||||
"must_be_valid_url": "The source must be a valid URL"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
@ -121,7 +121,8 @@
|
||||
"refuse_nsfw_content": "Voltar",
|
||||
"stats": "Estatísticas",
|
||||
"download_count": "Downloads",
|
||||
"player_count": "Jogadores ativos"
|
||||
"player_count": "Jogadores ativos",
|
||||
"download_error": "Essa opção de download falhou"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
@ -206,7 +207,10 @@
|
||||
"friends_only": "Apenas amigos",
|
||||
"public": "Público",
|
||||
"profile_visibility": "Visibilidade do perfil",
|
||||
"profile_visibility_description": "Escolha quem pode ver seu perfil e biblioteca"
|
||||
"profile_visibility_description": "Escolha quem pode ver seu perfil e biblioteca",
|
||||
"required_field": "Este campo é obrigatório",
|
||||
"source_already_exists": "Essa fonte já foi adicionada",
|
||||
"must_be_valid_url": "A fonte deve ser uma URL válida"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
|
@ -45,7 +45,7 @@ import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./auth/get-session-hash";
|
||||
import "./user/get-user";
|
||||
import "./user/get-user-blocks";
|
||||
import "./user/get-blocked-users";
|
||||
import "./user/block-user";
|
||||
import "./user/unblock-user";
|
||||
import "./user/get-user-friends";
|
||||
|
@ -1,9 +1,3 @@
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
repackRepository,
|
||||
} from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
@ -14,6 +8,8 @@ import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game, Repack } from "@main/entity";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -22,88 +18,95 @@ const startGameDownload = async (
|
||||
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
||||
payload;
|
||||
|
||||
const [game, repack] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||
const repackRepository = transactionalEntityManager.getRepository(Repack);
|
||||
const downloadQueueRepository =
|
||||
transactionalEntityManager.getRepository(DownloadQueue);
|
||||
|
||||
const [game, repack] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
shop,
|
||||
},
|
||||
}),
|
||||
repackRepository.findOne({
|
||||
where: {
|
||||
id: repackId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!repack) return;
|
||||
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository.update(
|
||||
{ status: "active", progress: Not(1) },
|
||||
{ status: "paused" }
|
||||
);
|
||||
|
||||
if (game) {
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository
|
||||
.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
})
|
||||
.then((result) => {
|
||||
if (iconUrl) {
|
||||
getFileBase64(iconUrl).then((base64) =>
|
||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
shop,
|
||||
},
|
||||
}),
|
||||
repackRepository.findOne({
|
||||
where: {
|
||||
id: repackId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!repack) return;
|
||||
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository.update(
|
||||
{ status: "active", progress: Not(1) },
|
||||
{ status: "paused" }
|
||||
);
|
||||
|
||||
if (game) {
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
createGame(updatedGame!).catch(() => {});
|
||||
|
||||
await gameRepository
|
||||
.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
})
|
||||
.then((result) => {
|
||||
if (iconUrl) {
|
||||
getFileBase64(iconUrl).then((base64) =>
|
||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
||||
);
|
||||
}
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
},
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
});
|
||||
|
||||
createGame(updatedGame!).catch(() => {});
|
||||
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
};
|
||||
|
||||
registerEvent("startGameDownload", startGameDownload);
|
||||
|
@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { UserBlocks } from "@types";
|
||||
|
||||
export const getUserBlocks = async (
|
||||
export const getBlockedUsers = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
take: number,
|
||||
skip: number
|
||||
@ -10,4 +10,4 @@ export const getUserBlocks = async (
|
||||
return HydraApi.get(`/profile/blocks`, { take, skip });
|
||||
};
|
||||
|
||||
registerEvent("getUserBlocks", getUserBlocks);
|
||||
registerEvent("getBlockedUsers", getBlockedUsers);
|
@ -159,8 +159,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||
ipcRenderer.invoke("getUserFriends", userId, take, skip),
|
||||
getUserBlocks: (take: number, skip: number) =>
|
||||
ipcRenderer.invoke("getUserBlocks", take, skip),
|
||||
getBlockedUsers: (take: number, skip: number) =>
|
||||
ipcRenderer.invoke("getBlockedUsers", take, skip),
|
||||
|
||||
/* Auth */
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { ComplexStyleRule, globalStyle, style } from "@vanilla-extract/css";
|
||||
import {
|
||||
ComplexStyleRule,
|
||||
createContainer,
|
||||
globalStyle,
|
||||
style,
|
||||
} from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "./theme.css";
|
||||
|
||||
export const appContainer = createContainer();
|
||||
|
||||
globalStyle("*", {
|
||||
boxSizing: "border-box",
|
||||
});
|
||||
@ -90,6 +97,8 @@ export const container = style({
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
containerName: appContainer,
|
||||
containerType: "inline-size",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
|
@ -2,16 +2,21 @@ import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 10_000;
|
||||
|
||||
export function SidebarProfile() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { t } = useTranslation("sidebar");
|
||||
|
||||
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
|
||||
const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
|
||||
useUserDetails();
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
@ -28,6 +33,18 @@ export function SidebarProfile() {
|
||||
navigate(`/profile/${userDetails!.id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pollingInterval.current = setInterval(() => {
|
||||
fetchFriendRequests();
|
||||
}, LONG_POLLING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
if (pollingInterval.current) {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
};
|
||||
}, [fetchFriendRequests]);
|
||||
|
||||
const friendsButton = useMemo(() => {
|
||||
if (!userDetails) return null;
|
||||
|
||||
|
2
src/renderer/src/declaration.d.ts
vendored
2
src/renderer/src/declaration.d.ts
vendored
@ -142,7 +142,7 @@ declare global {
|
||||
take: number,
|
||||
skip: number
|
||||
) => Promise<UserFriends>;
|
||||
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>;
|
||||
getBlockedUsers: (take: number, skip: number) => Promise<UserBlocks>;
|
||||
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
|
@ -25,11 +25,10 @@ export function useDownload() {
|
||||
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||
dispatch(clearDownload());
|
||||
|
||||
return window.electron.startGameDownload(payload).then((game) => {
|
||||
updateLibrary();
|
||||
const game = await window.electron.startGameDownload(payload);
|
||||
|
||||
return game;
|
||||
});
|
||||
await updateLibrary();
|
||||
return game;
|
||||
};
|
||||
|
||||
const pauseDownload = async (gameId: number) => {
|
||||
|
@ -10,7 +10,7 @@ import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
|
||||
export interface DownloadSettingsModalProps {
|
||||
visible: boolean;
|
||||
@ -31,6 +31,8 @@ export function DownloadSettingsModal({
|
||||
}: DownloadSettingsModalProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { showErrorToast } = useToast();
|
||||
|
||||
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
|
||||
const [selectedPath, setSelectedPath] = useState("");
|
||||
const [downloadStarting, setDownloadStarting] = useState(false);
|
||||
@ -104,10 +106,16 @@ export function DownloadSettingsModal({
|
||||
if (repack) {
|
||||
setDownloadStarting(true);
|
||||
|
||||
startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
|
||||
setDownloadStarting(false);
|
||||
onClose();
|
||||
});
|
||||
startDownload(repack, selectedDownloader!, selectedPath)
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
showErrorToast(t("download_error"));
|
||||
})
|
||||
.finally(() => {
|
||||
setDownloadStarting(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -67,6 +67,7 @@ export function EditProfileModal(
|
||||
return patchUser(values)
|
||||
.then(async () => {
|
||||
await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
|
||||
props.onClose();
|
||||
showSuccessToast(t("saved_successfully"));
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { appContainer } from "../../../app.css";
|
||||
import { vars, SPACING_UNIT } from "../../../theme.css";
|
||||
import { globalStyle, style } from "@vanilla-extract/css";
|
||||
|
||||
@ -73,11 +74,8 @@ export const rightContent = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
flexDirection: "column",
|
||||
transition: "all ease 0.2s",
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
width: "100%",
|
||||
maxWidth: "200px",
|
||||
},
|
||||
"(min-width: 1024px)": {
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
@ -108,20 +106,27 @@ export const listItem = style({
|
||||
|
||||
export const gamesGrid = style({
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
display: "grid",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
"@container": {
|
||||
[`${appContainer} (min-width: 1000px)`]: {
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
},
|
||||
"(min-width: 1250px)": {
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
[`${appContainer} (min-width: 1300px)`]: {
|
||||
gridTemplateColumns: "repeat(5, 1fr)",
|
||||
},
|
||||
"(min-width: 1600px)": {
|
||||
[`${appContainer} (min-width: 2000px)`]: {
|
||||
gridTemplateColumns: "repeat(6, 1fr)",
|
||||
},
|
||||
[`${appContainer} (min-width: 2600px)`]: {
|
||||
gridTemplateColumns: "repeat(8, 1fr)",
|
||||
},
|
||||
[`${appContainer} (min-width: 3000px)`]: {
|
||||
gridTemplateColumns: "repeat(12, 1fr)",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { steamUrlBuilder } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-content.css";
|
||||
import { ClockIcon } from "@primer/octicons-react";
|
||||
import { ClockIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||
import { Link } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserGame } from "@types";
|
||||
@ -71,6 +71,18 @@ export function ProfileContent() {
|
||||
return <LockedProfile />;
|
||||
}
|
||||
|
||||
if (userProfile.libraryGames.length === 0) {
|
||||
return (
|
||||
<div className={styles.noGames}>
|
||||
<div className={styles.telescopeIcon}>
|
||||
<TelescopeIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_recent_activity_title")}</h2>
|
||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
@ -83,17 +95,11 @@ export function ProfileContent() {
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>{t("library")}</h2>
|
||||
|
||||
<h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3>
|
||||
<span>
|
||||
{numberFormatter.format(userProfile.libraryGames.length)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* <div className={styles.noGames}>
|
||||
<div className={styles.telescopeIcon}>
|
||||
<TelescopeIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_recent_activity_title")}</h2>
|
||||
{isMe && <p>{t("no_recent_activity_description")}</p>}
|
||||
</div> */}
|
||||
|
||||
<ul className={styles.gamesGrid}>
|
||||
{userProfile?.libraryGames?.map((game) => (
|
||||
<li
|
||||
@ -129,60 +135,72 @@ export function ProfileContent() {
|
||||
</div>
|
||||
|
||||
<div className={styles.rightContent}>
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>{t("activity")}</h2>
|
||||
</div>
|
||||
{userProfile?.recentGames?.length > 0 && (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>{t("activity")}</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className={styles.listItem}
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl!}
|
||||
alt={game.title}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className={styles.listItem}
|
||||
>
|
||||
<span style={{ fontWeight: "bold" }}>{game.title}</span>
|
||||
<img
|
||||
src={game.iconUrl!}
|
||||
alt={game.title}
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<ClockIcon />
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{game.title}
|
||||
</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<ClockIcon />
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>{t("friends")}</h2>
|
||||
<span>{userProfile?.totalFriends}</span>
|
||||
<span>{numberFormatter.format(userProfile?.totalFriends)}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
@ -197,8 +215,8 @@ export function ProfileContent() {
|
||||
src={friend.profileImageUrl!}
|
||||
alt={friend.displayName}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
|
@ -48,6 +48,9 @@ export const profileDisplayName = style({
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const heroPanel = style({
|
||||
|
@ -26,6 +26,7 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { FriendRequestAction } from "@types";
|
||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
type FriendAction =
|
||||
| FriendRequestAction
|
||||
@ -35,7 +36,8 @@ export function ProfileHero() {
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||
|
||||
const context = useContext(userProfileContext);
|
||||
const { isMe, heroBackground, getUserProfile, userProfile } =
|
||||
useContext(userProfileContext);
|
||||
const {
|
||||
signOut,
|
||||
updateFriendRequestState,
|
||||
@ -46,10 +48,6 @@ export function ProfileHero() {
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const { isMe, heroBackground, getUserProfile } = context;
|
||||
|
||||
const userProfile = context.userProfile!;
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
@ -72,6 +70,7 @@ export function ProfileHero() {
|
||||
|
||||
const handleFriendAction = useCallback(
|
||||
async (userId: string, action: FriendAction) => {
|
||||
if (!userProfile) return;
|
||||
setIsPerformingAction(true);
|
||||
|
||||
try {
|
||||
@ -111,11 +110,13 @@ export function ProfileHero() {
|
||||
getUserProfile,
|
||||
navigate,
|
||||
showSuccessToast,
|
||||
userProfile.id,
|
||||
userProfile,
|
||||
]
|
||||
);
|
||||
|
||||
const profileActions = useMemo(() => {
|
||||
if (!userProfile) return null;
|
||||
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
@ -239,7 +240,7 @@ export function ProfileHero() {
|
||||
|
||||
return null;
|
||||
}
|
||||
return userProfile.currentGame;
|
||||
return userProfile?.currentGame;
|
||||
}, [isMe, userProfile, gameRunning]);
|
||||
|
||||
return (
|
||||
@ -267,11 +268,11 @@ export function ProfileHero() {
|
||||
className={styles.profileAvatarButton}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
{userProfile.profileImageUrl ? (
|
||||
{userProfile?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile.displayName}
|
||||
src={userProfile.profileImageUrl}
|
||||
alt={userProfile?.displayName}
|
||||
src={userProfile?.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
@ -279,9 +280,13 @@ export function ProfileHero() {
|
||||
</button>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile.displayName}
|
||||
</h2>
|
||||
{userProfile ? (
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile?.displayName}
|
||||
</h2>
|
||||
) : (
|
||||
<Skeleton width={150} height={28} />
|
||||
)}
|
||||
|
||||
{currentGame && (
|
||||
<div className={styles.currentGameWrapper}>
|
||||
|
@ -1,41 +0,0 @@
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ProfileSkeleton() {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton />
|
||||
<div>
|
||||
<div>
|
||||
<h2>{t("activity")}</h2>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
height={72}
|
||||
style={{ flex: "1", width: "100%" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>{t("library")}</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton key={index} style={{ aspectRatio: "1" }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { appContainer } from "@renderer/app.css";
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ProfileSkeleton } from "./profile-skeleton";
|
||||
import { ProfileContent } from "./profile-content/profile-content";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile.css";
|
||||
import {
|
||||
UserProfileContextConsumer,
|
||||
UserProfileContextProvider,
|
||||
} from "@renderer/context";
|
||||
import { UserProfileContextProvider } from "@renderer/context";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export function Profile() {
|
||||
const { userId } = useParams();
|
||||
@ -17,11 +13,7 @@ export function Profile() {
|
||||
<UserProfileContextProvider userId={userId!}>
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.wrapper}>
|
||||
<UserProfileContextConsumer>
|
||||
{({ userProfile }) =>
|
||||
userProfile ? <ProfileContent /> : <ProfileSkeleton />
|
||||
}
|
||||
</UserProfileContextConsumer>
|
||||
<ProfileContent />
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
</UserProfileContextProvider>
|
||||
|
@ -4,6 +4,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
||||
interface AddDownloadSourceModalProps {
|
||||
visible: boolean;
|
||||
@ -11,47 +15,83 @@ interface AddDownloadSourceModalProps {
|
||||
onAddDownloadSource: () => void;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function AddDownloadSourceModal({
|
||||
visible,
|
||||
onClose,
|
||||
onAddDownloadSource,
|
||||
}: AddDownloadSourceModalProps) {
|
||||
const [value, setValue] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const schema = yup.object().shape({
|
||||
url: yup.string().required(t("required_field")).url(t("must_be_valid_url")),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const [validationResult, setValidationResult] = useState<{
|
||||
name: string;
|
||||
downloadCount: number;
|
||||
} | null>(null);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const { sourceUrl } = useContext(settingsContext);
|
||||
|
||||
const handleValidateDownloadSource = useCallback(async (url: string) => {
|
||||
setIsLoading(true);
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormValues) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(url);
|
||||
setValidationResult(result);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
try {
|
||||
const result = await window.electron.validateDownloadSource(values.url);
|
||||
setValidationResult(result);
|
||||
|
||||
setUrl(values.url);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (
|
||||
error.message.endsWith("Source with the same url already exists")
|
||||
) {
|
||||
setError("url", {
|
||||
type: "server",
|
||||
message: t("source_already_exists"),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[setError, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("");
|
||||
setValue("url", "");
|
||||
clearErrors();
|
||||
setIsLoading(false);
|
||||
setValidationResult(null);
|
||||
|
||||
if (sourceUrl) {
|
||||
setValue(sourceUrl);
|
||||
handleValidateDownloadSource(sourceUrl);
|
||||
setValue("url", sourceUrl);
|
||||
handleSubmit(onSubmit)();
|
||||
}
|
||||
}, [visible, handleValidateDownloadSource, sourceUrl]);
|
||||
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
|
||||
|
||||
const handleAddDownloadSource = async () => {
|
||||
await window.electron.addDownloadSource(value);
|
||||
await window.electron.addDownloadSource(url);
|
||||
onClose();
|
||||
onAddDownloadSource();
|
||||
};
|
||||
@ -72,17 +112,17 @@ export function AddDownloadSourceModal({
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
{...register("url")}
|
||||
label={t("download_source_url")}
|
||||
placeholder={t("insert_valid_json_url")}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
error={errors.url}
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={() => handleValidateDownloadSource(value)}
|
||||
disabled={isLoading || !value}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
</Button>
|
||||
@ -115,7 +155,11 @@ export function AddDownloadSourceModal({
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<Button type="button" onClick={handleAddDownloadSource}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddDownloadSource}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t("import")}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -134,28 +134,27 @@ export function SettingsGeneral() {
|
||||
/>
|
||||
|
||||
<h3>{t("notifications")}</h3>
|
||||
<>
|
||||
<CheckboxField
|
||||
label={t("enable_download_notifications")}
|
||||
checked={form.downloadNotificationsEnabled}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<CheckboxField
|
||||
label={t("enable_repack_list_notifications")}
|
||||
checked={form.repackUpdatesNotificationsEnabled}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
repackUpdatesNotificationsEnabled:
|
||||
!form.repackUpdatesNotificationsEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<CheckboxField
|
||||
label={t("enable_download_notifications")}
|
||||
checked={form.downloadNotificationsEnabled}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<CheckboxField
|
||||
label={t("enable_repack_list_notifications")}
|
||||
checked={form.repackUpdatesNotificationsEnabled}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
repackUpdatesNotificationsEnabled:
|
||||
!form.repackUpdatesNotificationsEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,31 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const form = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const blockedUserAvatar = style({
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const blockedUser = style({
|
||||
display: "flex",
|
||||
minWidth: "240px",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const unblockButton = style({
|
||||
color: vars.color.muted,
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
@ -5,7 +5,8 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./settings-privacy.css";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { XCircleFillIcon, XIcon } from "@primer/octicons-react";
|
||||
|
||||
interface FormValues {
|
||||
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||
@ -25,12 +26,22 @@ export function SettingsPrivacy() {
|
||||
|
||||
const { patchUser, userDetails } = useUserDetails();
|
||||
|
||||
const [blockedUsers, setBlockedUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails?.profileVisibility) {
|
||||
setValue("profileVisibility", userDetails.profileVisibility);
|
||||
}
|
||||
}, [userDetails, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getBlockedUsers(12, 0).then((users) => {
|
||||
setBlockedUsers(users.blocks);
|
||||
});
|
||||
}, []);
|
||||
|
||||
console.log("BLOCKED USERS", blockedUsers);
|
||||
|
||||
const visibilityOptions = [
|
||||
{ value: "PUBLIC", label: t("public") },
|
||||
{ value: "FRIENDS", label: t("friends_only") },
|
||||
@ -47,31 +58,71 @@ export function SettingsPrivacy() {
|
||||
<Controller
|
||||
control={control}
|
||||
name="profileVisibility"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<SelectField
|
||||
label={t("profile_visibility")}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={visibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
/>
|
||||
render={({ field }) => {
|
||||
const handleChange = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
field.onChange(event);
|
||||
handleSubmit(onSubmit)();
|
||||
};
|
||||
|
||||
<small>{t("profile_visibility_description")}</small>
|
||||
</>
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
<SelectField
|
||||
label={t("profile_visibility")}
|
||||
value={field.value}
|
||||
onChange={handleChange}
|
||||
options={visibilityOptions.map((visiblity) => ({
|
||||
key: visiblity.value,
|
||||
value: visiblity.value,
|
||||
label: visiblity.label,
|
||||
}))}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<small>{t("profile_visibility_description")}</small>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
|
||||
disabled={isSubmitting}
|
||||
<h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
||||
Usuários bloqueados
|
||||
</h3>
|
||||
|
||||
<ul
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
listStyle: "none",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{t("save_changes")}
|
||||
</Button>
|
||||
{blockedUsers.map((user) => {
|
||||
return (
|
||||
<li key={user.id} className={styles.blockedUser}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
className={styles.blockedUserAvatar}
|
||||
/>
|
||||
<span>{user.displayName}</span>
|
||||
</div>
|
||||
|
||||
<button type="button" className={styles.unblockButton}>
|
||||
<XCircleFillIcon />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user