From a121ef77c0bdcbb7bdd07e7f471e87ca4a044575 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 1 Jan 2025 21:32:22 +0000 Subject: [PATCH 1/3] feat: adding translation for button --- package.json | 2 +- src/locales/en/translation.json | 3 +- src/locales/pt-BR/translation.json | 3 +- .../hardware/check-folder-write-permission.ts | 15 ++++ .../events/hardware/get-disk-free-space.ts | 4 +- src/main/events/index.ts | 4 +- src/main/events/misc/get-features.ts | 8 ++ src/main/services/window-manager.ts | 19 ++-- src/preload/index.ts | 3 + src/renderer/src/components/modal/modal.tsx | 6 ++ .../src/components/text-field/text-field.tsx | 8 +- src/renderer/src/declaration.d.ts | 6 +- src/renderer/src/hooks/index.ts | 1 + src/renderer/src/hooks/use-feature.ts | 23 +++++ src/renderer/src/hooks/use-user-details.ts | 7 ++ .../modals/download-settings-modal.css.ts | 7 ++ .../modals/download-settings-modal.tsx | 90 ++++++++++++------- .../edit-profile-modal/edit-profile-modal.tsx | 2 +- .../profile/report-profile/report-profile.tsx | 2 +- .../settings/add-download-source-modal.tsx | 2 +- yarn.lock | 23 +++-- 21 files changed, 176 insertions(+), 62 deletions(-) create mode 100644 src/main/events/hardware/check-folder-write-permission.ts create mode 100644 src/main/events/misc/get-features.ts create mode 100644 src/renderer/src/hooks/use-feature.ts diff --git a/package.json b/package.json index 897ba00c..7e838c4a 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,13 @@ "auto-launch": "^5.0.6", "axios": "^1.7.9", "better-sqlite3": "^11.7.0", - "check-disk-space": "^3.4.0", "classnames": "^2.5.1", "color": "^4.2.3", "color.js": "^1.2.0", "create-desktop-shortcuts": "^1.11.0", "date-fns": "^3.6.0", "dexie": "^4.0.10", + "diskusage": "^1.2.0", "electron-log": "^5.2.4", "electron-updater": "^6.3.9", "file-type": "^19.6.0", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c93cad1a..a164308c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -178,7 +178,8 @@ "select_folder": "Select folder", "backup_from": "Backup from {{date}}", "custom_backup_location_set": "Custom backup location set", - "no_directory_selected": "No directory selected" + "no_directory_selected": "No directory selected", + "no_write_permission": "Cannot download into this directory. Click here to learn more." }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 1c880176..203eef81 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -167,7 +167,8 @@ "select_folder": "Selecione a pasta", "manage_files_description": "Gerencie quais arquivos serão feitos backup", "clear": "Limpar", - "no_directory_selected": "Nenhum diretório selecionado" + "no_directory_selected": "Nenhum diretório selecionado", + "no_write_permission": "Его нельзя загрузить из этого каталога. Нажмите здесь, чтобы узнать больше." }, "activation": { "title": "Ativação", diff --git a/src/main/events/hardware/check-folder-write-permission.ts b/src/main/events/hardware/check-folder-write-permission.ts new file mode 100644 index 00000000..c74f01e7 --- /dev/null +++ b/src/main/events/hardware/check-folder-write-permission.ts @@ -0,0 +1,15 @@ +import fs from "node:fs"; + +import { registerEvent } from "../register-event"; + +const checkFolderWritePermission = async ( + _event: Electron.IpcMainInvokeEvent, + path: string +) => + new Promise((resolve) => { + fs.access(path, fs.constants.W_OK, (err) => { + resolve(!err); + }); + }); + +registerEvent("checkFolderWritePermission", checkFolderWritePermission); diff --git a/src/main/events/hardware/get-disk-free-space.ts b/src/main/events/hardware/get-disk-free-space.ts index ca591865..b5ac86e3 100644 --- a/src/main/events/hardware/get-disk-free-space.ts +++ b/src/main/events/hardware/get-disk-free-space.ts @@ -1,10 +1,10 @@ -import checkDiskSpace from "check-disk-space"; +import disk from "diskusage"; import { registerEvent } from "../register-event"; const getDiskFreeSpace = async ( _event: Electron.IpcMainInvokeEvent, path: string -) => checkDiskSpace(path); +) => disk.check(path); registerEvent("getDiskFreeSpace", getDiskFreeSpace); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index d4053974..68944060 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -11,6 +11,7 @@ import "./catalogue/get-trending-games"; import "./catalogue/get-publishers"; import "./catalogue/get-developers"; import "./hardware/get-disk-free-space"; +import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; import "./library/close-game"; @@ -30,6 +31,8 @@ import "./library/select-game-wine-prefix"; import "./misc/open-checkout"; import "./misc/open-external"; import "./misc/show-open-dialog"; +import "./misc/get-features"; +import "./misc/show-item-in-folder"; import "./torrenting/cancel-game-download"; import "./torrenting/pause-game-download"; import "./torrenting/resume-game-download"; @@ -71,7 +74,6 @@ import "./cloud-save/delete-game-artifact"; import "./cloud-save/select-game-backup-path"; import "./notifications/publish-new-repacks-notification"; import { isPortableVersion } from "@main/helpers"; -import "./misc/show-item-in-folder"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => appVersion); diff --git a/src/main/events/misc/get-features.ts b/src/main/events/misc/get-features.ts new file mode 100644 index 00000000..766c84aa --- /dev/null +++ b/src/main/events/misc/get-features.ts @@ -0,0 +1,8 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; + +const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => { + return HydraApi.get("/features", null, { needsAuth: false }); +}; + +registerEvent("getFeatures", getFeatures); diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index adc2f301..a7cfcee2 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -64,7 +64,10 @@ export class WindowManager { this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( (details, callback) => { - if (details.webContentsId !== this.mainWindow?.webContents.id) { + if ( + details.webContentsId !== this.mainWindow?.webContents.id || + details.url.includes("chatwoot") + ) { return callback(details); } @@ -81,15 +84,11 @@ export class WindowManager { this.mainWindow.webContents.session.webRequest.onHeadersReceived( (details, callback) => { - if (details.webContentsId !== this.mainWindow?.webContents.id) { - return callback(details); - } - - if (details.url.includes("featurebase")) { - return callback(details); - } - - if (details.url.includes("chatwoot")) { + if ( + details.webContentsId !== this.mainWindow?.webContents.id || + details.url.includes("featurebase") || + details.url.includes("chatwoot") + ) { return callback(details); } diff --git a/src/preload/index.ts b/src/preload/index.ts index 2a8ed69e..7b555000 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -150,6 +150,8 @@ contextBridge.exposeInMainWorld("electron", { /* Hardware */ getDiskFreeSpace: (path: string) => ipcRenderer.invoke("getDiskFreeSpace", path), + checkFolderWritePermission: (path: string) => + ipcRenderer.invoke("checkFolderWritePermission", path), /* Cloud save */ uploadSaveGame: ( @@ -226,6 +228,7 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("showOpenDialog", options), showItemInFolder: (path: string) => ipcRenderer.invoke("showItemInFolder", path), + getFeatures: () => ipcRenderer.invoke("getFeatures"), platform: process.platform, /* Auto update */ diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index af15feb5..d8d0554d 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -46,6 +46,12 @@ export function Modal({ }, [onClose]); const isTopMostModal = () => { + if ( + document.querySelector( + ".featurebase-widget-overlay.featurebase-display-block" + ) + ) + return false; const openModals = document.querySelectorAll("[role=dialog]"); return ( diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx index d4dfa007..32664e03 100644 --- a/src/renderer/src/components/text-field/text-field.tsx +++ b/src/renderer/src/components/text-field/text-field.tsx @@ -1,6 +1,5 @@ import React, { useId, useMemo, useState } from "react"; import type { RecipeVariants } from "@vanilla-extract/recipes"; -import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form"; import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; @@ -23,7 +22,7 @@ export interface TextFieldProps HTMLDivElement >; rightContent?: React.ReactNode | null; - error?: FieldError | Merge> | undefined; + error?: string | React.ReactNode; } export const TextField = React.forwardRef( @@ -55,10 +54,7 @@ export const TextField = React.forwardRef( }, [props.type, isPasswordVisible]); const hintContent = useMemo(() => { - if (error && error.message) - return ( - {error.message as string} - ); + if (error) return {error}; if (hint) return {hint}; return null; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index feec8284..88a16665 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -31,7 +31,7 @@ import type { CatalogueSearchPayload, } from "@types"; import type { AxiosProgressEvent } from "axios"; -import type { DiskSpace } from "check-disk-space"; +import type disk from "diskusage"; declare global { declare module "*.svg" { @@ -140,7 +140,8 @@ declare global { ) => Promise<{ fingerprint: string }>; /* Hardware */ - getDiskFreeSpace: (path: string) => Promise; + getDiskFreeSpace: (path: string) => Promise; + checkFolderWritePermission: (path: string) => Promise; /* Cloud save */ uploadSaveGame: ( @@ -195,6 +196,7 @@ declare global { options: Electron.OpenDialogOptions ) => Promise; showItemInFolder: (path: string) => Promise; + getFeatures: () => Promise; platform: NodeJS.Platform; /* Auto update */ diff --git a/src/renderer/src/hooks/index.ts b/src/renderer/src/hooks/index.ts index 97f519ef..8140e0cd 100644 --- a/src/renderer/src/hooks/index.ts +++ b/src/renderer/src/hooks/index.ts @@ -6,3 +6,4 @@ export * from "./redux"; export * from "./use-user-details"; export * from "./use-format"; export * from "./use-repacks"; +export * from "./use-feature"; diff --git a/src/renderer/src/hooks/use-feature.ts b/src/renderer/src/hooks/use-feature.ts new file mode 100644 index 00000000..ea682ce4 --- /dev/null +++ b/src/renderer/src/hooks/use-feature.ts @@ -0,0 +1,23 @@ +import { useEffect } from "react"; + +enum Feature { + CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION", +} + +export function useFeature() { + useEffect(() => { + window.electron.getFeatures().then((features) => { + localStorage.setItem("features", JSON.stringify(features || [])); + }); + }, []); + + const isFeatureEnabled = (feature: Feature) => { + const features = JSON.parse(localStorage.getItem("features") || "[]"); + return features.includes(feature); + }; + + return { + isFeatureEnabled, + Feature, + }; +} diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 3328c517..43636ecd 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -13,6 +13,7 @@ import type { UpdateProfileRequest, UserDetails, } from "@types"; +import * as Sentry from "@sentry/react"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { isFuture, isToday } from "date-fns"; @@ -44,6 +45,12 @@ export function useUserDetails() { const updateUserDetails = useCallback( async (userDetails: UserDetails) => { + Sentry.setUser({ + id: userDetails.id, + username: userDetails.username, + email: userDetails.email ?? undefined, + }); + dispatch(setUserDetails(userDetails)); window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); }, diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts index 5450378c..3a776736 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts @@ -36,3 +36,10 @@ export const downloaderIcon = style({ position: "absolute", left: `${SPACING_UNIT * 2}px`, }); + +export const pathError = style({ + cursor: "pointer", + ":hover": { + textDecoration: "underline", + }, +}); 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 191d9ac1..541bd01c 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 @@ -1,7 +1,6 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { DiskSpace } from "check-disk-space"; import * as styles from "./download-settings-modal.css"; import { Button, Link, Modal, TextField } from "@renderer/components"; import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; @@ -10,7 +9,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, useToast } from "@renderer/hooks"; +import { useAppSelector, useFeature, useToast } from "@renderer/hooks"; export interface DownloadSettingsModalProps { visible: boolean; @@ -33,21 +32,45 @@ export function DownloadSettingsModal({ const { showErrorToast } = useToast(); - const [diskFreeSpace, setDiskFreeSpace] = useState(null); + const [diskFreeSpace, setDiskFreeSpace] = useState(null); const [selectedPath, setSelectedPath] = useState(""); const [downloadStarting, setDownloadStarting] = useState(false); const [selectedDownloader, setSelectedDownloader] = useState(null); + const [hasWritePermission, setHasWritePermission] = useState( + null + ); + + const { isFeatureEnabled, Feature } = useFeature(); const userPreferences = useAppSelector( (state) => state.userPreferences.value ); + const getDiskFreeSpace = (path: string) => { + window.electron.getDiskFreeSpace(path).then((result) => { + setDiskFreeSpace(result.free); + }); + }; + + const checkFolderWritePermission = useCallback( + async (path: string) => { + if (isFeatureEnabled(Feature.CheckDownloadWritePermission)) { + const result = await window.electron.checkFolderWritePermission(path); + setHasWritePermission(result); + } else { + setHasWritePermission(true); + } + }, + [Feature, isFeatureEnabled] + ); + useEffect(() => { if (visible) { getDiskFreeSpace(selectedPath); + checkFolderWritePermission(selectedPath); } - }, [visible, selectedPath]); + }, [visible, checkFolderWritePermission, selectedPath]); const downloaders = useMemo(() => { return getDownloadersForUris(repack?.uris ?? []); @@ -84,12 +107,6 @@ export function DownloadSettingsModal({ userPreferences?.realDebridApiToken, ]); - const getDiskFreeSpace = (path: string) => { - window.electron.getDiskFreeSpace(path).then((result) => { - setDiskFreeSpace(result); - }); - }; - const handleChooseDownloadsPath = async () => { const { filePaths } = await window.electron.showOpenDialog({ defaultPath: selectedPath, @@ -124,7 +141,7 @@ export function DownloadSettingsModal({ visible={visible} title={t("download_settings")} description={t("space_left_on_disk", { - space: formatBytes(diskFreeSpace?.free ?? 0), + space: formatBytes(diskFreeSpace ?? 0), })} onClose={onClose} > @@ -168,23 +185,32 @@ export function DownloadSettingsModal({ gap: `${SPACING_UNIT}px`, }} > -
- - - -
+ + {t("no_write_permission")} + + ) : undefined + } + rightContent={ + + } + />

@@ -195,7 +221,11 @@ export function DownloadSettingsModal({