feat: fixing download error for gofile

This commit is contained in:
Chubby Granny Chaser 2024-09-14 18:10:02 +01:00
parent 702b141f7b
commit b122489b34
No known key found for this signature in database
23 changed files with 441 additions and 297 deletions

View file

@ -125,7 +125,8 @@
"refuse_nsfw_content": "Go back", "refuse_nsfw_content": "Go back",
"stats": "Stats", "stats": "Stats",
"download_count": "Downloads", "download_count": "Downloads",
"player_count": "Active players" "player_count": "Active players",
"download_error": "This download option is not available"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@ -207,7 +208,10 @@
"friends_only": "Friends only", "friends_only": "Friends only",
"privacy": "Privacy", "privacy": "Privacy",
"profile_visibility": "Profile visibility", "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": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",

View file

@ -121,7 +121,8 @@
"refuse_nsfw_content": "Voltar", "refuse_nsfw_content": "Voltar",
"stats": "Estatísticas", "stats": "Estatísticas",
"download_count": "Downloads", "download_count": "Downloads",
"player_count": "Jogadores ativos" "player_count": "Jogadores ativos",
"download_error": "Essa opção de download falhou"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",
@ -206,7 +207,10 @@
"friends_only": "Apenas amigos", "friends_only": "Apenas amigos",
"public": "Público", "public": "Público",
"profile_visibility": "Visibilidade do perfil", "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": { "notifications": {
"download_complete": "Download concluído", "download_complete": "Download concluído",

View file

@ -45,7 +45,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/get-blocked-users";
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";

View file

@ -1,9 +1,3 @@
import {
downloadQueueRepository,
gameRepository,
repackRepository,
} from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { StartGameDownloadPayload } from "@types"; import type { StartGameDownloadPayload } from "@types";
@ -14,6 +8,8 @@ import { Not } from "typeorm";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync"; import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared"; import { steamUrlBuilder } from "@shared";
import { dataSource } from "@main/data-source";
import { DownloadQueue, Game, Repack } from "@main/entity";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -22,6 +18,12 @@ const startGameDownload = async (
const { repackId, objectID, title, shop, downloadPath, downloader, uri } = const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
payload; payload;
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([ const [game, repack] = await Promise.all([
gameRepository.findOne({ gameRepository.findOne({
where: { where: {
@ -99,11 +101,12 @@ const startGameDownload = async (
createGame(updatedGame!).catch(() => {}); createGame(updatedGame!).catch(() => {});
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
await DownloadManager.cancelDownload(updatedGame!.id); await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!); await DownloadManager.startDownload(updatedGame!);
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
});
}; };
registerEvent("startGameDownload", startGameDownload); registerEvent("startGameDownload", startGameDownload);

View file

@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
import { UserBlocks } from "@types"; import { UserBlocks } from "@types";
export const getUserBlocks = async ( export const getBlockedUsers = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
take: number, take: number,
skip: number skip: number
@ -10,4 +10,4 @@ export const getUserBlocks = async (
return HydraApi.get(`/profile/blocks`, { take, skip }); return HydraApi.get(`/profile/blocks`, { take, skip });
}; };
registerEvent("getUserBlocks", getUserBlocks); registerEvent("getBlockedUsers", getBlockedUsers);

View file

@ -159,8 +159,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) => getBlockedUsers: (take: number, skip: number) =>
ipcRenderer.invoke("getUserBlocks", take, skip), ipcRenderer.invoke("getBlockedUsers", take, skip),
/* Auth */ /* Auth */
signOut: () => ipcRenderer.invoke("signOut"), signOut: () => ipcRenderer.invoke("signOut"),

View file

@ -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"; import { SPACING_UNIT, vars } from "./theme.css";
export const appContainer = createContainer();
globalStyle("*", { globalStyle("*", {
boxSizing: "border-box", boxSizing: "border-box",
}); });
@ -90,6 +97,8 @@ export const container = style({
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
containerName: appContainer,
containerType: "inline-size",
}); });
export const content = style({ export const content = style({

View file

@ -2,16 +2,21 @@ import { useNavigate } from "react-router-dom";
import { PeopleIcon, PersonIcon } from "@primer/octicons-react"; import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css"; import * as styles from "./sidebar-profile.css";
import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useMemo } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
const LONG_POLLING_INTERVAL = 10_000;
export function SidebarProfile() { export function SidebarProfile() {
const navigate = useNavigate(); const navigate = useNavigate();
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
const { t } = useTranslation("sidebar"); const { t } = useTranslation("sidebar");
const { userDetails, friendRequests, showFriendsModal } = useUserDetails(); const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
useUserDetails();
const { gameRunning } = useAppSelector((state) => state.gameRunning); const { gameRunning } = useAppSelector((state) => state.gameRunning);
@ -28,6 +33,18 @@ export function SidebarProfile() {
navigate(`/profile/${userDetails!.id}`); navigate(`/profile/${userDetails!.id}`);
}; };
useEffect(() => {
pollingInterval.current = setInterval(() => {
fetchFriendRequests();
}, LONG_POLLING_INTERVAL);
return () => {
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
}
};
}, [fetchFriendRequests]);
const friendsButton = useMemo(() => { const friendsButton = useMemo(() => {
if (!userDetails) return null; if (!userDetails) return null;

View file

@ -142,7 +142,7 @@ declare global {
take: number, take: number,
skip: number skip: number
) => Promise<UserFriends>; ) => Promise<UserFriends>;
getUserBlocks: (take: number, skip: number) => Promise<UserBlocks>; getBlockedUsers: (take: number, skip: number) => Promise<UserBlocks>;
/* Profile */ /* Profile */
getMe: () => Promise<UserProfile | null>; getMe: () => Promise<UserProfile | null>;

View file

@ -25,11 +25,10 @@ export function useDownload() {
const startDownload = async (payload: StartGameDownloadPayload) => { const startDownload = async (payload: StartGameDownloadPayload) => {
dispatch(clearDownload()); dispatch(clearDownload());
return window.electron.startGameDownload(payload).then((game) => { const game = await window.electron.startGameDownload(payload);
updateLibrary();
await updateLibrary();
return game; return game;
});
}; };
const pauseDownload = async (gameId: number) => { const pauseDownload = async (gameId: number) => {

View file

@ -10,7 +10,7 @@ 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";
import { DOWNLOADER_NAME } from "@renderer/constants"; import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector } from "@renderer/hooks"; import { useAppSelector, useToast } from "@renderer/hooks";
export interface DownloadSettingsModalProps { export interface DownloadSettingsModalProps {
visible: boolean; visible: boolean;
@ -31,6 +31,8 @@ export function DownloadSettingsModal({
}: DownloadSettingsModalProps) { }: DownloadSettingsModalProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const { showErrorToast } = useToast();
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null); const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false); const [downloadStarting, setDownloadStarting] = useState(false);
@ -104,9 +106,15 @@ export function DownloadSettingsModal({
if (repack) { if (repack) {
setDownloadStarting(true); setDownloadStarting(true);
startDownload(repack, selectedDownloader!, selectedPath).finally(() => { startDownload(repack, selectedDownloader!, selectedPath)
setDownloadStarting(false); .then(() => {
onClose(); onClose();
})
.catch(() => {
showErrorToast(t("download_error"));
})
.finally(() => {
setDownloadStarting(false);
}); });
} }
}; };

View file

@ -67,6 +67,7 @@ export function EditProfileModal(
return patchUser(values) return patchUser(values)
.then(async () => { .then(async () => {
await Promise.allSettled([fetchUserDetails(), getUserProfile()]); await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
props.onClose();
showSuccessToast(t("saved_successfully")); showSuccessToast(t("saved_successfully"));
}) })
.catch(() => { .catch(() => {

View file

@ -1,3 +1,4 @@
import { appContainer } from "../../../app.css";
import { vars, SPACING_UNIT } from "../../../theme.css"; import { vars, SPACING_UNIT } from "../../../theme.css";
import { globalStyle, style } from "@vanilla-extract/css"; import { globalStyle, style } from "@vanilla-extract/css";
@ -73,11 +74,8 @@ export const rightContent = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT * 2}px`,
flexDirection: "column", flexDirection: "column",
transition: "all ease 0.2s",
"@media": { "@media": {
"(min-width: 768px)": {
width: "100%",
maxWidth: "200px",
},
"(min-width: 1024px)": { "(min-width: 1024px)": {
maxWidth: "300px", maxWidth: "300px",
width: "100%", width: "100%",
@ -108,20 +106,27 @@ export const listItem = style({
export const gamesGrid = style({ export const gamesGrid = style({
listStyle: "none", listStyle: "none",
margin: 0, margin: "0",
padding: 0, padding: "0",
display: "grid", display: "grid",
gap: `${SPACING_UNIT * 2}px`, 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)": { [`${appContainer} (min-width: 1300px)`]: {
gridTemplateColumns: "repeat(3, 1fr)", gridTemplateColumns: "repeat(5, 1fr)",
}, },
"(min-width: 1600px)": { [`${appContainer} (min-width: 2000px)`]: {
gridTemplateColumns: "repeat(6, 1fr)",
},
[`${appContainer} (min-width: 2600px)`]: {
gridTemplateColumns: "repeat(8, 1fr)", gridTemplateColumns: "repeat(8, 1fr)",
}, },
[`${appContainer} (min-width: 3000px)`]: {
gridTemplateColumns: "repeat(12, 1fr)",
},
}, },
}); });

View file

@ -7,7 +7,7 @@ import { steamUrlBuilder } from "@shared";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./profile-content.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 { Link } from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserGame } from "@types"; import { UserGame } from "@types";
@ -71,6 +71,18 @@ export function ProfileContent() {
return <LockedProfile />; 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 ( return (
<section <section
style={{ style={{
@ -83,17 +95,11 @@ export function ProfileContent() {
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>{t("library")}</h2> <h2>{t("library")}</h2>
<h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3> <span>
{numberFormatter.format(userProfile.libraryGames.length)}
</span>
</div> </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}> <ul className={styles.gamesGrid}>
{userProfile?.libraryGames?.map((game) => ( {userProfile?.libraryGames?.map((game) => (
<li <li
@ -129,6 +135,7 @@ export function ProfileContent() {
</div> </div>
<div className={styles.rightContent}> <div className={styles.rightContent}>
{userProfile?.recentGames?.length > 0 && (
<div> <div>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>{t("activity")}</h2> <h2>{t("activity")}</h2>
@ -146,8 +153,8 @@ export function ProfileContent() {
src={game.iconUrl!} src={game.iconUrl!}
alt={game.title} alt={game.title}
style={{ style={{
width: "30px", width: "32px",
height: "30px", height: "32px",
borderRadius: "4px", borderRadius: "4px",
}} }}
/> />
@ -157,9 +164,19 @@ export function ProfileContent() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT / 2}px`, gap: `${SPACING_UNIT / 2}px`,
overflow: "hidden",
}} }}
> >
<span style={{ fontWeight: "bold" }}>{game.title}</span> <span
style={{
fontWeight: "bold",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
>
{game.title}
</span>
<div <div
style={{ style={{
@ -178,11 +195,12 @@ export function ProfileContent() {
</ul> </ul>
</div> </div>
</div> </div>
)}
<div> <div>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>{t("friends")}</h2> <h2>{t("friends")}</h2>
<span>{userProfile?.totalFriends}</span> <span>{numberFormatter.format(userProfile?.totalFriends)}</span>
</div> </div>
<div className={styles.box}> <div className={styles.box}>
@ -197,8 +215,8 @@ export function ProfileContent() {
src={friend.profileImageUrl!} src={friend.profileImageUrl!}
alt={friend.displayName} alt={friend.displayName}
style={{ style={{
width: "30px", width: "32px",
height: "30px", height: "32px",
borderRadius: "4px", borderRadius: "4px",
}} }}
/> />

View file

@ -48,6 +48,9 @@ export const profileDisplayName = style({
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
width: "100%", width: "100%",
display: "flex",
alignItems: "center",
position: "relative",
}); });
export const heroPanel = style({ export const heroPanel = style({

View file

@ -26,6 +26,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 Skeleton from "react-loading-skeleton";
type FriendAction = type FriendAction =
| FriendRequestAction | FriendRequestAction
@ -35,7 +36,8 @@ export function ProfileHero() {
const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [isPerformingAction, setIsPerformingAction] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false);
const context = useContext(userProfileContext); const { isMe, heroBackground, getUserProfile, userProfile } =
useContext(userProfileContext);
const { const {
signOut, signOut,
updateFriendRequestState, updateFriendRequestState,
@ -46,10 +48,6 @@ export function ProfileHero() {
const { gameRunning } = useAppSelector((state) => state.gameRunning); const { gameRunning } = useAppSelector((state) => state.gameRunning);
const { isMe, heroBackground, getUserProfile } = context;
const userProfile = context.userProfile!;
const { t } = useTranslation("user_profile"); const { t } = useTranslation("user_profile");
const { formatDistance } = useDate(); const { formatDistance } = useDate();
@ -72,6 +70,7 @@ export function ProfileHero() {
const handleFriendAction = useCallback( const handleFriendAction = useCallback(
async (userId: string, action: FriendAction) => { async (userId: string, action: FriendAction) => {
if (!userProfile) return;
setIsPerformingAction(true); setIsPerformingAction(true);
try { try {
@ -111,11 +110,13 @@ export function ProfileHero() {
getUserProfile, getUserProfile,
navigate, navigate,
showSuccessToast, showSuccessToast,
userProfile.id, userProfile,
] ]
); );
const profileActions = useMemo(() => { const profileActions = useMemo(() => {
if (!userProfile) return null;
if (isMe) { if (isMe) {
return ( return (
<> <>
@ -239,7 +240,7 @@ export function ProfileHero() {
return null; return null;
} }
return userProfile.currentGame; return userProfile?.currentGame;
}, [isMe, userProfile, gameRunning]); }, [isMe, userProfile, gameRunning]);
return ( return (
@ -267,11 +268,11 @@ export function ProfileHero() {
className={styles.profileAvatarButton} className={styles.profileAvatarButton}
onClick={handleAvatarClick} onClick={handleAvatarClick}
> >
{userProfile.profileImageUrl ? ( {userProfile?.profileImageUrl ? (
<img <img
className={styles.profileAvatar} className={styles.profileAvatar}
alt={userProfile.displayName} alt={userProfile?.displayName}
src={userProfile.profileImageUrl} src={userProfile?.profileImageUrl}
/> />
) : ( ) : (
<PersonIcon size={72} /> <PersonIcon size={72} />
@ -279,9 +280,13 @@ export function ProfileHero() {
</button> </button>
<div className={styles.profileInformation}> <div className={styles.profileInformation}>
{userProfile ? (
<h2 className={styles.profileDisplayName}> <h2 className={styles.profileDisplayName}>
{userProfile.displayName} {userProfile?.displayName}
</h2> </h2>
) : (
<Skeleton width={150} height={28} />
)}
{currentGame && ( {currentGame && (
<div className={styles.currentGameWrapper}> <div className={styles.currentGameWrapper}>

View file

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

View file

@ -1,3 +1,4 @@
import { appContainer } from "@renderer/app.css";
import { SPACING_UNIT } from "../../theme.css"; import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";

View file

@ -1,14 +1,10 @@
import { useParams } from "react-router-dom";
import { ProfileSkeleton } from "./profile-skeleton";
import { ProfileContent } from "./profile-content/profile-content"; import { ProfileContent } from "./profile-content/profile-content";
import { SkeletonTheme } from "react-loading-skeleton"; import { SkeletonTheme } from "react-loading-skeleton";
import { vars } from "@renderer/theme.css"; import { vars } from "@renderer/theme.css";
import * as styles from "./profile.css"; import * as styles from "./profile.css";
import { import { UserProfileContextProvider } from "@renderer/context";
UserProfileContextConsumer, import { useParams } from "react-router-dom";
UserProfileContextProvider,
} from "@renderer/context";
export function Profile() { export function Profile() {
const { userId } = useParams(); const { userId } = useParams();
@ -17,11 +13,7 @@ export function Profile() {
<UserProfileContextProvider userId={userId!}> <UserProfileContextProvider userId={userId!}>
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444"> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<div className={styles.wrapper}> <div className={styles.wrapper}>
<UserProfileContextConsumer> <ProfileContent />
{({ userProfile }) =>
userProfile ? <ProfileContent /> : <ProfileSkeleton />
}
</UserProfileContextConsumer>
</div> </div>
</SkeletonTheme> </SkeletonTheme>
</UserProfileContextProvider> </UserProfileContextProvider>

View file

@ -4,6 +4,10 @@ import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components"; import { Button, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
interface AddDownloadSourceModalProps { interface AddDownloadSourceModalProps {
visible: boolean; visible: boolean;
@ -11,47 +15,83 @@ interface AddDownloadSourceModalProps {
onAddDownloadSource: () => void; onAddDownloadSource: () => void;
} }
interface FormValues {
url: string;
}
export function AddDownloadSourceModal({ export function AddDownloadSourceModal({
visible, visible,
onClose, onClose,
onAddDownloadSource, onAddDownloadSource,
}: AddDownloadSourceModalProps) { }: AddDownloadSourceModalProps) {
const [value, setValue] = useState(""); const [url, setUrl] = useState("");
const [isLoading, setIsLoading] = useState(false); 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<{ const [validationResult, setValidationResult] = useState<{
name: string; name: string;
downloadCount: number; downloadCount: number;
} | null>(null); } | null>(null);
const { t } = useTranslation("settings");
const { sourceUrl } = useContext(settingsContext); const { sourceUrl } = useContext(settingsContext);
const handleValidateDownloadSource = useCallback(async (url: string) => { const onSubmit = useCallback(
async (values: FormValues) => {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await window.electron.validateDownloadSource(url); const result = await window.electron.validateDownloadSource(values.url);
setValidationResult(result); 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 { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, []); },
[setError, t]
);
useEffect(() => { useEffect(() => {
setValue(""); setValue("url", "");
clearErrors();
setIsLoading(false); setIsLoading(false);
setValidationResult(null); setValidationResult(null);
if (sourceUrl) { if (sourceUrl) {
setValue(sourceUrl); setValue("url", sourceUrl);
handleValidateDownloadSource(sourceUrl); handleSubmit(onSubmit)();
} }
}, [visible, handleValidateDownloadSource, sourceUrl]); }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
const handleAddDownloadSource = async () => { const handleAddDownloadSource = async () => {
await window.electron.addDownloadSource(value); await window.electron.addDownloadSource(url);
onClose(); onClose();
onAddDownloadSource(); onAddDownloadSource();
}; };
@ -72,17 +112,17 @@ export function AddDownloadSourceModal({
}} }}
> >
<TextField <TextField
{...register("url")}
label={t("download_source_url")} label={t("download_source_url")}
placeholder={t("insert_valid_json_url")} placeholder={t("insert_valid_json_url")}
value={value} error={errors.url}
onChange={(e) => setValue(e.target.value)}
rightContent={ rightContent={
<Button <Button
type="button" type="button"
theme="outline" theme="outline"
style={{ alignSelf: "flex-end" }} style={{ alignSelf: "flex-end" }}
onClick={() => handleValidateDownloadSource(value)} onClick={handleSubmit(onSubmit)}
disabled={isLoading || !value} disabled={isLoading}
> >
{t("validate_download_source")} {t("validate_download_source")}
</Button> </Button>
@ -115,7 +155,11 @@ export function AddDownloadSourceModal({
</small> </small>
</div> </div>
<Button type="button" onClick={handleAddDownloadSource}> <Button
type="button"
onClick={handleAddDownloadSource}
disabled={isLoading}
>
{t("import")} {t("import")}
</Button> </Button>
</div> </div>

View file

@ -134,7 +134,7 @@ export function SettingsGeneral() {
/> />
<h3>{t("notifications")}</h3> <h3>{t("notifications")}</h3>
<>
<CheckboxField <CheckboxField
label={t("enable_download_notifications")} label={t("enable_download_notifications")}
checked={form.downloadNotificationsEnabled} checked={form.downloadNotificationsEnabled}
@ -156,6 +156,5 @@ export function SettingsGeneral() {
} }
/> />
</> </>
</>
); );
} }

View file

@ -1,9 +1,31 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
export const form = style({ export const form = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`, 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",
});

View file

@ -5,7 +5,8 @@ import { useTranslation } from "react-i18next";
import * as styles from "./settings-privacy.css"; import * as styles from "./settings-privacy.css";
import { useToast, useUserDetails } from "@renderer/hooks"; import { useToast, useUserDetails } from "@renderer/hooks";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { XCircleFillIcon, XIcon } from "@primer/octicons-react";
interface FormValues { interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE"; profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
@ -25,12 +26,22 @@ export function SettingsPrivacy() {
const { patchUser, userDetails } = useUserDetails(); const { patchUser, userDetails } = useUserDetails();
const [blockedUsers, setBlockedUsers] = useState([]);
useEffect(() => { useEffect(() => {
if (userDetails?.profileVisibility) { if (userDetails?.profileVisibility) {
setValue("profileVisibility", userDetails.profileVisibility); setValue("profileVisibility", userDetails.profileVisibility);
} }
}, [userDetails, setValue]); }, [userDetails, setValue]);
useEffect(() => {
window.electron.getBlockedUsers(12, 0).then((users) => {
setBlockedUsers(users.blocks);
});
}, []);
console.log("BLOCKED USERS", blockedUsers);
const visibilityOptions = [ const visibilityOptions = [
{ value: "PUBLIC", label: t("public") }, { value: "PUBLIC", label: t("public") },
{ value: "FRIENDS", label: t("friends_only") }, { value: "FRIENDS", label: t("friends_only") },
@ -47,31 +58,71 @@ export function SettingsPrivacy() {
<Controller <Controller
control={control} control={control}
name="profileVisibility" name="profileVisibility"
render={({ field }) => ( render={({ field }) => {
const handleChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
field.onChange(event);
handleSubmit(onSubmit)();
};
return (
<> <>
<SelectField <SelectField
label={t("profile_visibility")} label={t("profile_visibility")}
value={field.value} value={field.value}
onChange={field.onChange} onChange={handleChange}
options={visibilityOptions.map((visiblity) => ({ options={visibilityOptions.map((visiblity) => ({
key: visiblity.value, key: visiblity.value,
value: visiblity.value, value: visiblity.value,
label: visiblity.label, label: visiblity.label,
}))} }))}
disabled={isSubmitting}
/> />
<small>{t("profile_visibility_description")}</small> <small>{t("profile_visibility_description")}</small>
</> </>
)} );
}}
/> />
<Button <h3 style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
type="submit" Usuários bloqueados
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }} </h3>
disabled={isSubmitting}
<ul
style={{
padding: 0,
margin: 0,
listStyle: "none",
display: "flex",
}}
> >
{t("save_changes")} {blockedUsers.map((user) => {
</Button> 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> </form>
); );
} }