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",
"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",

View File

@ -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",

View File

@ -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";

View File

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

View File

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

View File

@ -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"),

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";
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({

View File

@ -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;

View File

@ -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>;

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

@ -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",
}}
/>

View File

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

View File

@ -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}>

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 { 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 { 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>

View File

@ -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>

View File

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

View File

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

View File

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