diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0055a8b3..73eadcd9 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index e04e5c7f..ce5f4c6c 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 66adab39..e271a19b 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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"; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 90350f4a..a24e0ffb 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -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); diff --git a/src/main/events/user/get-user-blocks.ts b/src/main/events/user/get-blocked-users.ts similarity index 76% rename from src/main/events/user/get-user-blocks.ts rename to src/main/events/user/get-blocked-users.ts index 65bb3eb4..3d213898 100644 --- a/src/main/events/user/get-user-blocks.ts +++ b/src/main/events/user/get-blocked-users.ts @@ -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); diff --git a/src/preload/index.ts b/src/preload/index.ts index 5103e333..51498d4f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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"), diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts index c829021a..4e0cf7a0 100644 --- a/src/renderer/src/app.css.ts +++ b/src/renderer/src/app.css.ts @@ -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({ diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index 652d932e..d8cbf656 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -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(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; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index aa5b1d9f..70d6d463 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -142,7 +142,7 @@ declare global { take: number, skip: number ) => Promise; - getUserBlocks: (take: number, skip: number) => Promise; + getBlockedUsers: (take: number, skip: number) => Promise; /* Profile */ getMe: () => Promise; diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 07c885cf..31c3bf2f 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -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) => { diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 3450af24..4ea2569c 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -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(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); + }); } }; diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx index a9f06e20..cd43641a 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx @@ -67,6 +67,7 @@ export function EditProfileModal( return patchUser(values) .then(async () => { await Promise.allSettled([fetchUserDetails(), getUserProfile()]); + props.onClose(); showSuccessToast(t("saved_successfully")); }) .catch(() => { diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts index 0a140c1a..a0fb9979 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts +++ b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts @@ -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)", + }, }, }); diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 7f3887fb..a0db7404 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -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 ; } + if (userProfile.libraryGames.length === 0) { + return ( +
+
+ +
+

{t("no_recent_activity_title")}

+ {isMe &&

{t("no_recent_activity_description")}

} +
+ ); + } + return (

{t("library")}

-

{numberFormatter.format(userProfile.libraryGames.length)}

+ + {numberFormatter.format(userProfile.libraryGames.length)} + - {/*
-
- -
-

{t("no_recent_activity_title")}

- {isMe &&

{t("no_recent_activity_description")}

} -
*/} -
    {userProfile?.libraryGames?.map((game) => (
  • -
    -
    -

    {t("activity")}

    -
    + {userProfile?.recentGames?.length > 0 && ( +
    +
    +

    {t("activity")}

    +
    -
    -
      - {userProfile?.recentGames.map((game) => ( -
    • - - {game.title} - -
      +
        + {userProfile?.recentGames.map((game) => ( +
      • + - {game.title} + {game.title}
        - - {formatPlayTime(game)} + + {game.title} + + +
        + + {formatPlayTime(game)} +
        -
      - -
    • - ))} -
    + +
  • + ))} +
+ - + )}

{t("friends")}

- {userProfile?.totalFriends} + {numberFormatter.format(userProfile?.totalFriends)}
@@ -197,8 +215,8 @@ export function ProfileContent() { src={friend.profileImageUrl!} alt={friend.displayName} style={{ - width: "30px", - height: "30px", + width: "32px", + height: "32px", borderRadius: "4px", }} /> diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index 5d2d2018..5ef6cc75 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -48,6 +48,9 @@ export const profileDisplayName = style({ overflow: "hidden", textOverflow: "ellipsis", width: "100%", + display: "flex", + alignItems: "center", + position: "relative", }); export const heroPanel = style({ diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 14dca9d0..89c49852 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -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 ? ( {userProfile.displayName} ) : ( @@ -279,9 +280,13 @@ export function ProfileHero() {
-

- {userProfile.displayName} -

+ {userProfile ? ( +

+ {userProfile?.displayName} +

+ ) : ( + + )} {currentGame && (
diff --git a/src/renderer/src/pages/profile/profile-skeleton.tsx b/src/renderer/src/pages/profile/profile-skeleton.tsx deleted file mode 100644 index faececf3..00000000 --- a/src/renderer/src/pages/profile/profile-skeleton.tsx +++ /dev/null @@ -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 ( - <> - -
-
-

{t("activity")}

- {Array.from({ length: 3 }).map((_, index) => ( - - ))} -
- -
-

{t("library")}

-
- {Array.from({ length: 8 }).map((_, index) => ( - - ))} -
-
-
- - ); -} diff --git a/src/renderer/src/pages/profile/profile.css.ts b/src/renderer/src/pages/profile/profile.css.ts index 68f5fa56..e04ade7b 100644 --- a/src/renderer/src/pages/profile/profile.css.ts +++ b/src/renderer/src/pages/profile/profile.css.ts @@ -1,3 +1,4 @@ +import { appContainer } from "@renderer/app.css"; import { SPACING_UNIT } from "../../theme.css"; import { style } from "@vanilla-extract/css"; diff --git a/src/renderer/src/pages/profile/profile.tsx b/src/renderer/src/pages/profile/profile.tsx index 86399cd6..7da18346 100644 --- a/src/renderer/src/pages/profile/profile.tsx +++ b/src/renderer/src/pages/profile/profile.tsx @@ -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() {
- - {({ userProfile }) => - userProfile ? : - } - +
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 98495c67..015ee0dc 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -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({ + 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({ }} > setValue(e.target.value)} + error={errors.url} rightContent={ @@ -115,7 +155,11 @@ export function AddDownloadSourceModal({
-
diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 4aab85d4..a363f55d 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -134,28 +134,27 @@ export function SettingsGeneral() { />

{t("notifications")}

- <> - - handleChange({ - downloadNotificationsEnabled: !form.downloadNotificationsEnabled, - }) - } - /> - - handleChange({ - repackUpdatesNotificationsEnabled: - !form.repackUpdatesNotificationsEnabled, - }) - } - /> - + + handleChange({ + downloadNotificationsEnabled: !form.downloadNotificationsEnabled, + }) + } + /> + + + handleChange({ + repackUpdatesNotificationsEnabled: + !form.repackUpdatesNotificationsEnabled, + }) + } + /> ); } diff --git a/src/renderer/src/pages/settings/settings-privacy.css.ts b/src/renderer/src/pages/settings/settings-privacy.css.ts index 73763780..a0638e68 100644 --- a/src/renderer/src/pages/settings/settings-privacy.css.ts +++ b/src/renderer/src/pages/settings/settings-privacy.css.ts @@ -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", +}); diff --git a/src/renderer/src/pages/settings/settings-privacy.tsx b/src/renderer/src/pages/settings/settings-privacy.tsx index 5fb67d8e..69603cad 100644 --- a/src/renderer/src/pages/settings/settings-privacy.tsx +++ b/src/renderer/src/pages/settings/settings-privacy.tsx @@ -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() { ( - <> - ({ - key: visiblity.value, - value: visiblity.value, - label: visiblity.label, - }))} - /> + render={({ field }) => { + const handleChange = ( + event: React.ChangeEvent + ) => { + field.onChange(event); + handleSubmit(onSubmit)(); + }; - {t("profile_visibility_description")} - - )} + return ( + <> + ({ + key: visiblity.value, + value: visiblity.value, + label: visiblity.label, + }))} + disabled={isSubmitting} + /> + + {t("profile_visibility_description")} + + ); + }} /> - + {blockedUsers.map((user) => { + return ( +
  • +
    + {user.displayName} + {user.displayName} +
    + + +
  • + ); + })} + ); }