diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 1f45cabe..489a741c 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -80,7 +80,7 @@ "PreReleaseUpdatesDescription": "Disabled by default. Enable to update and test pre-release versions of the app. Pre-releases are more likely to have issues.", "YouAreRunningNiceNode": "You are running NiceNode", "NoNotificationsYet": "No notifications yet", - "WellLetYouKnow": "We’ll let you know when something interesting happens!", + "WellLetYouKnow": "We'll let you know when something interesting happens!", "MarkAllAsRead": "Mark all as read", "ClearNotifications": "Clear notifications", "NotificationPreferences": "Notification preferences", @@ -97,7 +97,7 @@ "BlockExplorer": "Block Explorer", "BrowserExtensionId": "Browser Extension ID", "UnableSetWalletConnections": "Unable to set wallet connections for this node. This node is missing configuration values for this feature.", - "WalletDescription": "Connect your browser wallet to this node so you can enjoy greater security, privacy, and read speeds. Enable your favourite browser wallets below to allow access to your node. Don’t forget to add a new network in your wallet with the configuration below.", + "WalletDescription": "Connect your browser wallet to this node so you can enjoy greater security, privacy, and read speeds. Enable your favourite browser wallets below to allow access to your node. Don't forget to add a new network in your wallet with the configuration below.", "UsingLesserKnownWallet": "Using a lesser known wallet?", "SelectBrowser": "If your wallet of choice is not displayed in the list above you can select the browser used for the extension and provide the extension ID to allow access.", "AddRow": "Add Row", @@ -154,7 +154,7 @@ "NodeRequirements": "Node Requirements", "NodeStartCommand": "Node start command (must save changes to take effect)", "ResetToDefaults": "Reset to defaults", - "AddBaseNodeDescription": "Base is a secure, low-cost, developer-friendly Ethereum L2 built to bring the next billion users onchain. It's built on Optimism’s open-source OP Stack.", + "AddBaseNodeDescription": "Base is a secure, low-cost, developer-friendly Ethereum L2 built to bring the next billion users onchain. It's built on Optimism's open-source OP Stack.", "LaunchAVarNode": "Launch a {{nodeName}} Node", "AddNodeDescription": "Support the health of a network protocol by running a node.", "InitialSyncStarted": "Initial sync process started", @@ -180,5 +180,11 @@ "RunningLatestVersion": "You are running the latest version", "SuccessfullyUpdated": "Successfully updated", "Done": "Done", - "Close": "Close" + "Close": "Close", + "Storage": "Storage", + "DefaultNodeStorage": "Default Node Storage Location", + "DefaultNodeStorageDescription": "The default location where node data will be stored. This can be changed per node in node settings.", + "NoWritePermissions": "Permission Denied", + "NoWritePermissionsMessage": "Cannot write to selected folder", + "NoWritePermissionsDetail": "NiceNode does not have permission to write to the selected folder: {{path}}\n\nPlease select a different folder or check the folder permissions." } diff --git a/src/main/dialog.ts b/src/main/dialog.ts index 75c3c97a..9e7ff5cc 100644 --- a/src/main/dialog.ts +++ b/src/main/dialog.ts @@ -1,4 +1,5 @@ import { type BrowserWindow, dialog } from 'electron'; +import { access, constants } from 'node:fs/promises'; import type Node from '../common/node'; import type { NodeId } from '../common/node'; @@ -61,6 +62,18 @@ export const openDialogForNodeDataDir = async (nodeId: NodeId) => { return; }; +export const checkWritePermissions = async ( + folderPath: string, +): Promise => { + try { + await access(folderPath, constants.W_OK); + return true; + } catch (err) { + logger.error(`No write permissions for path ${folderPath}:`, err); + return false; + } +}; + export const openDialogForStorageLocation = async (): Promise< CheckStorageDetails | undefined > => { @@ -77,19 +90,32 @@ export const openDialogForStorageLocation = async (): Promise< defaultPath, properties: ['openDirectory'], }); - console.log('dir select result: ', result); + if (result.canceled) { return; } - if (result.filePaths) { - if (result.filePaths.length > 0) { - const folderPath = result.filePaths[0]; - const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); - return { - folderPath, - freeStorageGBs, - }; + + if (result.filePaths && result.filePaths.length > 0) { + const folderPath = result.filePaths[0]; + + // Check write permissions + const hasWritePermissions = await checkWritePermissions(folderPath); + if (!hasWritePermissions) { + // Show error dialog to user + await dialog.showMessageBox(mainWindow, { + type: 'error', + title: t('NoWritePermissions'), + message: t('NoWritePermissionsMessage'), + detail: t('NoWritePermissionsDetail', { path: folderPath }), + }); + return; } + + const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); + return { + folderPath, + freeStorageGBs, + }; } return; diff --git a/src/main/files.ts b/src/main/files.ts index 26b165ea..9d8f7ccc 100644 --- a/src/main/files.ts +++ b/src/main/files.ts @@ -8,6 +8,7 @@ import { app } from 'electron'; import logger from './logger'; import du from 'du'; +import store from './state/store'; logger.info(`App data dir: ${app.getPath('appData')}`); logger.info(`User data dir: ${app.getPath('userData')}`); @@ -27,7 +28,15 @@ export const getNNDirPath = (): string => { * @returns getNNDirPath + '/nodes' */ export const getNodesDirPath = (): string => { - return path.join(getNNDirPath(), 'nodes'); + // First check if user has set a custom default location + const userDefaultLocation = store.get('appDefaultStorageLocation'); + if (userDefaultLocation) { + return userDefaultLocation; + } + + // Fall back to default app storage location + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'nodes'); }; export const checkAndOrCreateDir = async (dirPath: string) => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index ab16994c..3068dfc4 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -83,6 +83,7 @@ import { import { getSetIsDeveloperModeEnabled } from './state/settings.js'; import store from './state/store'; import { getSystemInfo } from './systemInfo'; +import { setDefaultStorageLocation } from './state/settings'; export const initialize = () => { ipcMain.handle( @@ -285,4 +286,12 @@ export const initialize = () => { ipcMain.handle('checkPorts', (_event, ports: number[]) => { return checkPorts(ports); }); + + // Add to the initialize function + ipcMain.handle( + 'setDefaultStorageLocation', + (_event, storageLocation: string) => { + return setDefaultStorageLocation(storageLocation); + }, + ); }; diff --git a/src/main/preload.ts b/src/main/preload.ts index a6902a19..46ce6466 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -184,4 +184,7 @@ contextBridge.exposeInMainWorld('electron', { checkPorts: (ports: number[]) => { ipcRenderer.invoke('checkPorts', ports); }, + + setDefaultStorageLocation: (storageLocation: string) => + ipcRenderer.invoke('setDefaultStorageLocation', storageLocation), }); diff --git a/src/main/state/settings.ts b/src/main/state/settings.ts index d622a101..9e707d9b 100644 --- a/src/main/state/settings.ts +++ b/src/main/state/settings.ts @@ -24,9 +24,10 @@ const APP_IS_NOTIFICATIONS_ENABLED = 'appIsNotificationsEnabled'; export const APP_IS_EVENT_REPORTING_ENABLED = 'appIsEventReportingEnabled'; const APP_IS_PRE_RELEASE_UPDATES_ENABLED = 'appIsPreReleaseUpdatesEnabled'; export const APP_IS_DEVELOPER_MODE_ENABLED = 'appIsDeveloperModeEnabled'; +const APP_DEFAULT_STORAGE_LOCATION = 'appDefaultStorageLocation'; export type ThemeSetting = 'light' | 'dark' | 'auto'; -export type Settings = { +export interface Settings { [OS_PLATFORM_KEY]?: string; [OS_ARCHITECTURE]?: string; [OS_LANGUAGE_KEY]?: string; @@ -39,7 +40,8 @@ export type Settings = { [APP_IS_EVENT_REPORTING_ENABLED]?: boolean; [APP_IS_PRE_RELEASE_UPDATES_ENABLED]?: boolean; [APP_IS_DEVELOPER_MODE_ENABLED]?: boolean; -}; + [APP_DEFAULT_STORAGE_LOCATION]?: string; +} /** * Called on app launch. @@ -209,6 +211,10 @@ export const getSetIsDeveloperModeEnabled = ( return savedIsDeveloperModeEnabled; }; +export const setDefaultStorageLocation = (storageLocation: string) => { + store.set('appDefaultStorageLocation', storageLocation); +}; + // listen to OS theme updates nativeTheme.on('updated', () => { console.log("nativeTheme.on('updated')"); diff --git a/src/renderer/Presentational/Preferences/Preferences.tsx b/src/renderer/Presentational/Preferences/Preferences.tsx index 8975d201..0b8cc959 100644 --- a/src/renderer/Presentational/Preferences/Preferences.tsx +++ b/src/renderer/Presentational/Preferences/Preferences.tsx @@ -1,16 +1,17 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { HorizontalLine } from '../../Generics/redesign/HorizontalLine/HorizontalLine'; -import { Icon } from '../../Generics/redesign/Icon/Icon'; -import LineLabelSettings from '../../Generics/redesign/LabelSetting/LabelSettings'; -import { Toggle } from '../../Generics/redesign/Toggle/Toggle'; -import LanguageSelect from '../../LanguageSelect'; -import AutoDark from '../../assets/images/artwork/auto-dark.png'; -import AutoLight from '../../assets/images/artwork/auto-light.png'; -import DarkDark from '../../assets/images/artwork/dark-dark.png'; -import DarkLight from '../../assets/images/artwork/dark-light.png'; -import LightDark from '../../assets/images/artwork/light-dark.png'; -import LightLight from '../../assets/images/artwork/light-light.png'; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { HorizontalLine } from "../../Generics/redesign/HorizontalLine/HorizontalLine"; +import { Icon } from "../../Generics/redesign/Icon/Icon"; +import LineLabelSettings from "../../Generics/redesign/LabelSetting/LabelSettings"; +import { Toggle } from "../../Generics/redesign/Toggle/Toggle"; +import LanguageSelect from "../../LanguageSelect"; +import AutoDark from "../../assets/images/artwork/auto-dark.png"; +import AutoLight from "../../assets/images/artwork/auto-light.png"; +import DarkDark from "../../assets/images/artwork/dark-dark.png"; +import DarkLight from "../../assets/images/artwork/dark-light.png"; +import LightDark from "../../assets/images/artwork/light-dark.png"; +import LightLight from "../../assets/images/artwork/light-light.png"; +import FolderInput from "../../Generics/redesign/Input/FolderInput"; import { appearanceSection, captionText, @@ -26,17 +27,18 @@ import { themeImage, themeInnerContainer, versionContainer, -} from './preferences.css'; +} from "./preferences.css"; -export type ThemeSetting = 'light' | 'dark' | 'auto'; +export type ThemeSetting = "light" | "dark" | "auto"; export type Preference = - | 'theme' - | 'isOpenOnStartup' - | 'isNotificationsEnabled' - | 'isEventReportingEnabled' - | 'isPreReleaseUpdatesEnabled' - | 'isDeveloperModeEnabled' - | 'language'; + | "theme" + | "isOpenOnStartup" + | "isNotificationsEnabled" + | "isEventReportingEnabled" + | "isPreReleaseUpdatesEnabled" + | "isDeveloperModeEnabled" + | "language" + | "storageLocation"; export interface PreferencesProps { themeSetting?: ThemeSetting; isOpenOnStartup?: boolean; @@ -64,23 +66,40 @@ const Preferences = ({ }: PreferencesProps) => { const { t } = useTranslation(); const [initialThemeSetting] = useState(themeSetting); + const [sNodeStorageLocation, setNodeStorageLocation] = useState(""); + const [ + sNodeStorageLocationFreeStorageGBs, + setNodeStorageLocationFreeStorageGBs, + ] = useState(); + + useEffect(() => { + const fetchData = async () => { + const defaultNodesStorageDetails = + await electron.getNodesDefaultStorageLocation(); + setNodeStorageLocation(defaultNodesStorageDetails.folderPath); + setNodeStorageLocationFreeStorageGBs( + defaultNodesStorageDetails.freeStorageGBs + ); + }; + fetchData(); + }, []); const onClickTheme = (theme: ThemeSetting) => { if (onChange) { - onChange('theme', theme); + onChange("theme", theme); } }; const getThemeThumbnail = (theme: ThemeSetting) => { - const lightTheme = initialThemeSetting === 'light'; + const lightTheme = initialThemeSetting === "light"; switch (theme) { - case 'auto': - return !osDarkMode || initialThemeSetting === 'light' + case "auto": + return !osDarkMode || initialThemeSetting === "light" ? AutoLight : AutoDark; - case 'light': + case "light": return lightTheme ? LightLight : LightDark; - case 'dark': + case "dark": return lightTheme ? DarkLight : DarkDark; default: } @@ -89,20 +108,20 @@ const Preferences = ({ return (
-
{t('Appearance')}
+
{t("Appearance")}
{[ { - theme: 'auto', - label: t('AutoFollowsComputerSetting'), + theme: "auto", + label: t("AutoFollowsComputerSetting"), }, { - theme: 'light', - label: t('LightMode'), + theme: "light", + label: t("LightMode"), }, { - theme: 'dark', - label: t('DarkMode'), + theme: "dark", + label: t("DarkMode"), }, ].map((themeDetails, index) => { const isSelected = themeSetting === themeDetails.theme; @@ -113,7 +132,7 @@ const Preferences = ({ selectedStyle.push(selectedThemeContainer); } const thumbnail = getThemeThumbnail( - themeDetails.theme as ThemeSetting, + themeDetails.theme as ThemeSetting ); return (
onClickTheme(themeDetails.theme as ThemeSetting)} onKeyDown={() => onClickTheme(themeDetails.theme as ThemeSetting)} @@ -138,11 +157,11 @@ const Preferences = ({
)} -
+
{themeDetails.label}
@@ -152,36 +171,36 @@ const Preferences = ({ })}
-
{t('General')}
+
{t("General")}
{ if (onChange) { - onChange('isOpenOnStartup', newValue); + onChange("isOpenOnStartup", newValue); } }} /> ), }, { - label: t('Language'), + label: t("Language"), value: ( { if (onChange) { - onChange('language', newValue); + onChange("language", newValue); } }} /> @@ -194,24 +213,24 @@ const Preferences = ({
- {t('Notifications')} + {t("Notifications")}
{ if (onChange) { - onChange('isNotificationsEnabled', newValue); + onChange("isNotificationsEnabled", newValue); } }} /> @@ -223,29 +242,70 @@ const Preferences = ({ />
-
{t('Privacy')}
+
{t("Privacy")}
{ if (onChange) { - onChange('isEventReportingEnabled', newValue); + onChange("isEventReportingEnabled", newValue); + } + }} + /> + ), + learnMoreLink: "https://impact.nicenode.xyz", + }, + ], + }, + ]} + /> +
+
+
{t("Storage")}
+ + { + const storageLocationDetails = + await electron.openDialogForStorageLocation(); + if (storageLocationDetails) { + setNodeStorageLocation( + storageLocationDetails.folderPath + ); + setNodeStorageLocationFreeStorageGBs( + storageLocationDetails.freeStorageGBs + ); + if (onChange) { + onChange( + "storageLocation", + storageLocationDetails.folderPath + ); + } } }} /> ), - learnMoreLink: 'https://impact.nicenode.xyz', }, ], }, @@ -253,42 +313,42 @@ const Preferences = ({ />
-
{t('Advanced')}
+
{t("Advanced")}
{ if (onChange) { - onChange('isPreReleaseUpdatesEnabled', newValue); + onChange("isPreReleaseUpdatesEnabled", newValue); } }} /> ), }, { - label: `${t('Developer mode')}`, + label: `${t("Developer mode")}`, description: t( - 'Show developer information throuhout the app marked by 👷', + "Show developer information throuhout the app marked by 👷" ), value: ( { if (onChange) { - onChange('isDeveloperModeEnabled', newValue); + onChange("isDeveloperModeEnabled", newValue); } }} /> @@ -300,7 +360,7 @@ const Preferences = ({ />
- {t('YouAreRunningNiceNode')} {version} {import.meta.env.NICENODE_ENV} + {t("YouAreRunningNiceNode")} {version} {import.meta.env.NICENODE_ENV} {import.meta.env.MP_PROJECT_ENV}
diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 67055a64..2a882346 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -152,6 +152,9 @@ declare global { // Ports checkPorts(ports: number[]): void; + + // New method + setDefaultStorageLocation(storageLocation: string): void; }; performance: Performance;