diff --git a/assets/icons/tray/status/error.svg b/assets/icons/tray/status/error.svg new file mode 100644 index 000000000..998c06e2f --- /dev/null +++ b/assets/icons/tray/status/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tray/status/stopped.svg b/assets/icons/tray/status/stopped.svg new file mode 100644 index 000000000..62e03c0d3 --- /dev/null +++ b/assets/icons/tray/status/stopped.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tray/status/synced.svg b/assets/icons/tray/status/synced.svg new file mode 100644 index 000000000..1e99f1228 --- /dev/null +++ b/assets/icons/tray/status/synced.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tray/status/syncing.svg b/assets/icons/tray/status/syncing.svg new file mode 100644 index 000000000..c1185c77f --- /dev/null +++ b/assets/icons/tray/status/syncing.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/locales/en/systemRequirements.json b/assets/locales/en/systemRequirements.json index 919216bdc..8be9abf6f 100644 --- a/assets/locales/en/systemRequirements.json +++ b/assets/locales/en/systemRequirements.json @@ -1,4 +1,6 @@ { + "macOSTitle": "MacOS version is at least {{minVersion}} to run Podman 5.0", + "macOSDescription": "MacOS version: {{version}}", "processorCoresTitle": "Processor has {{minCores}} cores or more", "processorCoresDescription": "Processor cores: {{cores}}", "memorySizeTitle": "At least {{minSize}}GB of system memory (RAM)", diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 9242f684b..0fac93e54 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -168,5 +168,10 @@ "CalculatingDataSize": "(calculating data size...)", "KeepNodeData": "Keep node related chain data {{data}}", "NoResults": "No results", - "TrySearching": "Try searching for another keyword or clear all filters" + "TrySearching": "Try searching for another keyword or clear all filters", + "RunningOutdatedPodman": "You are running an outdated version of Podman", + "CurrentPodman": "Your current Podman installation ({{currentPodmanVersion}}) is incompatible with NiceNode and requires version {{requiredPodmanVersion}} or higher for it to run.", + "PodmanIsRequiredComponent": "Podman is a required component for NiceNode to run the many client options. Podman facilitates the running of containers within a virtualised Linux environment and will operate in the background.", + "DownloadAndUpdate": "Download and update", + "PodmanUpdate": "Podman update" } diff --git a/assets/trayIndex.html b/assets/trayIndex.html new file mode 100644 index 000000000..2b6f28205 --- /dev/null +++ b/assets/trayIndex.html @@ -0,0 +1,103 @@ + + + + + Tray Menu + + + + + + + diff --git a/assets/trayIndex.js b/assets/trayIndex.js new file mode 100644 index 000000000..ffa90bf82 --- /dev/null +++ b/assets/trayIndex.js @@ -0,0 +1,131 @@ +const { ipcRenderer } = require('electron'); + +const getIconKey = (status) => { + switch (status) { + case 'running': + case 'starting': + return 'syncing'; + //TODO: consider camelcased strings + case 'error running': + case 'error starting': + case 'error stopping': + case 'notInstalled': + case 'notRunning': + case 'isOutdated': + return 'error'; + default: + return status; + } +}; + +const getStatusText = (status) => { + switch (status) { + case 'notInstalled': + return 'Not Installed'; + case 'notRunning': + return 'Not Running'; + case 'isOutdated': + return 'Update Now'; + default: + return status; + } +}; + +ipcRenderer.on( + 'update-menu', + (event, { nodePackageTrayMenu, podmanMenuItem, statusIcons }) => { + const menuItems = [ + ...nodePackageTrayMenu.map((item) => ({ + name: item.name, + status: item.status, + action: () => ipcRenderer.send('node-package-click', item.id), + })), + ...(nodePackageTrayMenu.length >= 1 ? [{ separator: true }] : []), + ...(podmanMenuItem.status !== 'isRunning' + ? [ + { + name: 'Podman', + status: podmanMenuItem.status, + action: () => ipcRenderer.send('podman-click'), + }, + { separator: true }, + ] + : []), + { + name: 'Open NiceNode', + action: () => ipcRenderer.send('show-main-window'), + }, + { name: 'Quit', action: () => ipcRenderer.send('quit-app') }, + ]; + + const container = document.getElementById('menu-container'); + container.innerHTML = ''; // Clear existing items + + menuItems.forEach((item) => { + if (item.separator) { + const separator = document.createElement('div'); + separator.className = 'separator'; + container.appendChild(separator); + } else { + const menuItem = document.createElement('div'); + menuItem.className = 'menu-item'; + + if (item.status === 'stopped') { + menuItem.classList.add('stopped'); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = item.name; + menuItem.appendChild(nameSpan); + + if (item.status) { + const statusContainer = document.createElement('div'); + statusContainer.className = 'menu-status-container'; + + const statusIconContainer = document.createElement('div'); + statusIconContainer.className = 'menu-status-icon'; + + const statusIcon = document.createElement('div'); + statusIcon.innerHTML = + statusIcons[getIconKey(item.status)] || statusIcons.default; + statusIcon.className = 'status-icon'; + + const statusText = document.createElement('div'); + statusText.className = 'menu-status'; + statusText.textContent = getStatusText(item.status); + + statusIconContainer.appendChild(statusIcon); + statusContainer.appendChild(statusIconContainer); + statusContainer.appendChild(statusText); + menuItem.appendChild(statusContainer); + } + + menuItem.addEventListener('click', item.action); + + container.appendChild(menuItem); + } + }); + ipcRenderer.send('adjust-height', document.body.scrollHeight); + }, +); + +ipcRenderer.on('set-theme', (event, theme) => { + applyTheme(theme); +}); + +// Apply theme-based styles +const applyTheme = (theme) => { + const body = document.body; + const menuItems = document.querySelectorAll('.menu-item'); + if (theme === 'dark') { + body.classList.add('dark'); + body.classList.remove('light'); + } else { + body.classList.add('light'); + body.classList.remove('dark'); + } +}; + +ipcRenderer.on('update-menu', (event, updatedItems) => { + // Update menu items if necessary +}); diff --git a/src/main/messenger.ts b/src/main/messenger.ts index 95dbb7d8f..1064808a7 100644 --- a/src/main/messenger.ts +++ b/src/main/messenger.ts @@ -22,6 +22,8 @@ export const CHANNELS = { nodeLogs: 'nodeLogs', podman: 'podman', podmanInstall: 'podmanInstall', + openPodmanModal: 'openPodmanModal', + openNodePackageScreen: 'openNodePackageScreen', theme: 'theme', notifications: 'notifications', reportEvent: 'reportEvent', diff --git a/src/main/nn-auto-updater/main.ts b/src/main/nn-auto-updater/main.ts index 991190dd3..5bbb83f33 100644 --- a/src/main/nn-auto-updater/main.ts +++ b/src/main/nn-auto-updater/main.ts @@ -86,7 +86,11 @@ export class nnAutoUpdater if (isLinux()) { console.log('nnAutoUpdater setFeedURL in linux!'); } else { - this.nativeUpdater.setFeedURL(options); + try { + this.nativeUpdater.setFeedURL(options); + } catch (e) { + console.error('Error in setFeedURL: ', e); + } } } } diff --git a/src/main/nodePackageManager.ts b/src/main/nodePackageManager.ts index f80e8868a..5520054aa 100644 --- a/src/main/nodePackageManager.ts +++ b/src/main/nodePackageManager.ts @@ -144,6 +144,7 @@ export const startNodePackage = async (nodeId: NodeId) => { node.lastStartedTimestampMs = Date.now(); node.stoppedBy = undefined; nodePackageStore.updateNodePackage(node); + let allServicesStarted = true; const isEthereumPackage = node.spec.specId === 'ethereum'; @@ -153,6 +154,7 @@ export const startNodePackage = async (nodeId: NodeId) => { } catch (e) { logger.error(`Unable to start node service: ${JSON.stringify(service)}`); nodePackageStatus = NodeStatus.errorStarting; + allServicesStarted = false; // try to start all services, or stop other services? // todo: set as partially started? // throw e; @@ -182,7 +184,8 @@ export const startNodePackage = async (nodeId: NodeId) => { } // If all node services start without error, the package is considered running - if (nodePackageStatus === NodeStatus.running) { + if (allServicesStarted) { + nodePackageStatus = NodeStatus.running; setLastRunningTime(nodeId, 'node'); } diff --git a/src/main/podman/podman.ts b/src/main/podman/podman.ts index 9b4d4d3a8..eb4ed13cf 100644 --- a/src/main/podman/podman.ts +++ b/src/main/podman/podman.ts @@ -20,7 +20,7 @@ import { type ConfigValuesMap, buildCliConfig, } from '../../common/nodeConfig'; -import { send } from '../messenger'; +import { CHANNELS, send } from '../messenger.js'; import { restartNodes } from '../nodePackageManager'; import { isLinux } from '../platform'; import { killChildProcess } from '../processExit'; @@ -779,6 +779,10 @@ export const isPodmanStarting = async () => { return bIsPodmanStarting; }; +export const openPodmanModal = async () => { + send(CHANNELS.openPodmanModal); +}; + // todoo // setTimeout(() => { // isPodmanRunning(); diff --git a/src/main/state/nodePackages.ts b/src/main/state/nodePackages.ts index d63f59c4c..f64d5c36d 100644 --- a/src/main/state/nodePackages.ts +++ b/src/main/state/nodePackages.ts @@ -206,3 +206,7 @@ export const getUserNodePackagesWithNodes = async () => { }); return userNodePackages; }; + +export const openNodePackageScreen = async (nodeId: NodeId) => { + send(CHANNELS.openNodePackageScreen, nodeId); +}; diff --git a/src/main/tray.ts b/src/main/tray.ts index 1c0e1e905..ecfc5f3ae 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,20 +1,32 @@ -import { Menu, MenuItem, Tray } from 'electron'; +import fs from 'node:fs/promises'; +import { + BrowserWindow, + Menu, + MenuItem, + Tray, + dialog, + ipcMain, + nativeTheme, + screen, +} from 'electron'; +import { NodeStoppedBy } from '../common/node.js'; import logger from './logger'; import { createWindow, fullQuit, getMainWindow } from './main'; -import { isLinux, isWindows } from './platform'; -import { - getNiceNodeMachine, - startMachineIfCreated, - stopMachineIfCreated, -} from './podman/machine'; +import { stopAllNodePackages } from './nodePackageManager.js'; +import { isLinux, isMac, isWindows } from './platform'; +import { getPodmanDetails } from './podman/details.js'; +import { getNiceNodeMachine, stopMachineIfCreated } from './podman/machine'; +import { openPodmanModal } from './podman/podman.js'; import { getUserNodePackages } from './state/nodePackages'; +import { openNodePackageScreen } from './state/nodePackages.js'; // Can't import from main because of circular dependency let _getAssetPath: (...paths: string[]) => string; let tray: Tray; +let trayWindow: BrowserWindow | null = null; -// Can get asyncronously updated separately +// Can get asynchronously updated separately let nodePackageTrayMenu: { label: string; click: () => void }[] = []; let podmanTrayMenu: MenuItem[] = []; let openNiceNodeMenu: { label: string; click: () => void }[] = []; @@ -33,25 +45,58 @@ export const setTrayIcon = (style: 'Default' | 'Alert') => { } }; +const openOrFocusWindow = () => { + const mainWindow = getMainWindow(); + if (mainWindow === null) { + // Create a new window if none exists + createWindow(); + } else { + // Show and focus on the existing window + mainWindow.show(); + if (mainWindow.isMinimized()) { + mainWindow.restore(); // Restore if minimized + } + mainWindow.focus(); // Focus on the window + } +}; + export const setTrayMenu = () => { const menuTemplate = [ - // todo: change icon if there are any status errors + // Podman status with start, stop, and delete? ...nodePackageTrayMenu, { type: 'separator' }, - // todo: show in developer mode - - // todo: add podman status with start, stop, and delete? + ...podmanTrayMenu, + { type: 'separator' }, ...openNiceNodeMenu, { - label: 'Full Quit', + label: 'Quit', click: () => { - fullQuit(); // app no longer runs in the background + // Show confirmation dialog before quitting + // TODO: get translated strings for this + dialog + .showMessageBox({ + type: 'question', + buttons: ['Yes', 'No'], + defaultId: 1, // Focus on 'No' by default + title: 'Confirm', + message: 'Are you sure you want to quit? Nodes will stop syncing.', + detail: 'Confirming will close the application.', + }) + .then(async (result) => { + if (result.response === 0) { + // The 'Yes' button is at index 0 + await stopAllNodePackages(NodeStoppedBy.shutdown); + await stopMachineIfCreated(); + fullQuit(); // app no longer runs in the background + } + // Do nothing if the user selects 'No' + }) + .catch((err) => { + logger.error('Error showing dialog:', err); + }); }, }, ]; - if (process.env.NODE_ENV === 'development') { - menuTemplate.push(...podmanTrayMenu, { type: 'separator' }); - } const contextMenu = Menu.buildFromTemplate(menuTemplate as MenuItem[]); if (tray) { @@ -60,18 +105,14 @@ export const setTrayMenu = () => { }; const getOpenNiceNodeMenu = () => { - if (getMainWindow() === null) { - openNiceNodeMenu = [ - { - label: 'Open NiceNode', - click: () => { - createWindow(); // app no longer runs in the background - }, + openNiceNodeMenu = [ + { + label: 'Open NiceNode', + click: () => { + openOrFocusWindow(); }, - ]; - } else { - openNiceNodeMenu = []; - } + }, + ]; setTrayMenu(); }; @@ -84,8 +125,10 @@ const getNodePackageListMenu = () => { isAlert = true; } return { - label: `${nodePackage.spec.displayName} Node ${nodePackage.status}`, + label: `${nodePackage.spec.displayName} Node ${nodePackage.status}`, click: () => { + openOrFocusWindow(); + openNodePackageScreen(nodePackage.id); logger.info(`clicked on ${nodePackage.spec.displayName}`); }, }; @@ -126,8 +169,9 @@ const getPodmanMenuItem = async () => { // stop stopMachineIfCreated(); } else { - // try to start if any other start - startMachineIfCreated(); + openOrFocusWindow(); + openPodmanModal(); + // startMachineIfCreated(); } }, type: 'checkbox', @@ -143,23 +187,267 @@ export const updateTrayMenu = () => { getOpenNiceNodeMenu(); }; +function createCustomTrayWindow() { + trayWindow = new BrowserWindow({ + // width: 277, + height: 100, // Initial height + show: false, + frame: false, + resizable: false, + transparent: true, + webPreferences: { + contextIsolation: false, + nodeIntegration: true, + }, + vibrancy: isMac() ? 'sidebar' : undefined, + }); + + trayWindow.loadURL(`file://${_getAssetPath('trayIndex.html')}`); + + trayWindow.on('blur', () => { + if (trayWindow) { + trayWindow.hide(); + } + }); + + nativeTheme.on('updated', () => { + const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + trayWindow?.webContents.send('set-theme', theme); + }); + + trayWindow.webContents.on('did-finish-load', () => { + updateCustomTrayMenu(); + }); + + ipcMain.on('adjust-height', (event, height) => { + if (trayWindow) { + trayWindow.setSize(300, height); + } + }); + + ipcMain.on('node-package-click', (event, id) => { + openOrFocusWindow(); + openNodePackageScreen(id); + logger.info(`clicked on node package with id ${id}`); + }); + + ipcMain.on('podman-click', async (event) => { + const { status } = await getCustomPodmanMenuItem(); + logger.info('clicked on podman machine'); + if (status === 'Running' || status === 'Starting') { + stopMachineIfCreated(); + } else { + openOrFocusWindow(); + openPodmanModal(); + } + }); + + ipcMain.on('show-main-window', async (event) => { + logger.info('clicked on show-main-window'); + openOrFocusWindow(); + }); + + ipcMain.on('quit-app', (event) => { + dialog + .showMessageBox({ + type: 'question', + buttons: ['Yes', 'No'], + defaultId: 1, // Focus on 'No' by default + title: 'Confirm', + message: 'Are you sure you want to quit? Nodes will stop syncing.', + detail: 'Confirming will close the application.', + }) + .then(async (result) => { + if (result.response === 0) { + // The 'Yes' button is at index 0 + await stopAllNodePackages(NodeStoppedBy.shutdown); + await stopMachineIfCreated(); + fullQuit(); // app no longer runs in the background + } + // Do nothing if the user selects 'No' + }) + .catch((err) => { + logger.error('Error showing dialog:', err); + }); + }); +} + +const getCustomNodePackageListMenu = () => { + const userNodes = getUserNodePackages(); + let isAlert = false; + const nodePackageTrayMenu = userNodes.nodeIds.map((nodeId) => { + const nodePackage = userNodes.nodes[nodeId]; + if (nodePackage.status.toLowerCase().includes('error')) { + isAlert = true; + } + return { + //TODO: localization + name: `${nodePackage.spec.displayName} Node`, + status: nodePackage.status, + id: nodePackage.id, + }; + }); + + console.log('getCustomNodePackageListMenu'); + return { nodePackageTrayMenu, isAlert }; +}; + +const getCustomPodmanMenuItem = async () => { + if (isLinux()) { + return { status: 'N/A' }; + } + let status = 'notInstalled'; + try { + const podmanMachine = await getNiceNodeMachine(); + if (podmanMachine) { + const podmanDetails = await getPodmanDetails(); + switch (true) { + case !podmanDetails.isInstalled: + status = 'notInstalled'; + break; + case podmanDetails.isOutdated: + status = 'isOutdated'; + break; + case !podmanDetails.isRunning: + status = 'notRunning'; + break; + case podmanDetails.isRunning: + status = 'isRunning'; + break; + default: + status = 'isRunning'; + } + } + } catch (e) { + console.error('tray podmanMachine error: ', e); + status = 'Not found'; + } + return { status }; +}; + +const svgCache = new Map(); + +const readSVGContent = async (filePath: string) => { + if (svgCache.has(filePath)) { + return svgCache.get(filePath); + } + + try { + const data = await fs.readFile(filePath, 'utf8'); + svgCache.set(filePath, data); + return data; + } catch (error) { + console.error(`Error reading file from path ${filePath}`, error); + return ''; + } +}; + +export const updateCustomTrayMenu = async () => { + const { nodePackageTrayMenu, isAlert } = getCustomNodePackageListMenu(); + const podmanMenuItem = await getCustomPodmanMenuItem(); + + const statusIcons = { + synced: await readSVGContent( + _getAssetPath('icons', 'tray', 'status', 'synced.svg'), + ), + error: await readSVGContent( + _getAssetPath('icons', 'tray', 'status', 'error.svg'), + ), + syncing: await readSVGContent( + _getAssetPath('icons', 'tray', 'status', 'syncing.svg'), + ), + default: await readSVGContent( + _getAssetPath('icons', 'tray', 'status', 'syncing.svg'), + ), + stopped: await readSVGContent( + _getAssetPath('icons', 'tray', 'status', 'stopped.svg'), + ), + }; + + if (trayWindow) { + trayWindow.webContents.send('update-menu', { + nodePackageTrayMenu, + podmanMenuItem, + statusIcons, + }); + } + setTrayIcon(isAlert ? 'Alert' : 'Default'); +}; + +function toggleCustomTrayWindow() { + if (trayWindow.isVisible()) { + trayWindow.hide(); + } else { + const trayBounds = tray.getBounds(); // Get the bounds of the tray icon + const windowBounds = trayWindow.getBounds(); + let x; + let y; + + if (isMac()) { + x = Math.round( + trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2, + ); + y = Math.round(trayBounds.y + trayBounds.height); + } else if (isWindows()) { + const display = screen.getPrimaryDisplay(); // Get the primary display details + const workArea = display.workArea; + x = Math.round( + trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2, + ); + + // Check if taskbar is at the bottom or the top + if (workArea.y < trayBounds.y) { + y = Math.round(trayBounds.y - windowBounds.height); // Taskbar is at the bottom + } else { + y = Math.round(trayBounds.y + trayBounds.height); // Taskbar is at the top + } + } else { + // Assume Linux behaves like Windows in this context + // This could require adjustments based on the Linux distro and environment + const display = screen.getPrimaryDisplay(); + const workArea = display.workArea; + x = Math.round( + trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2, + ); + y = + workArea.y < trayBounds.y + ? Math.round(trayBounds.y - windowBounds.height) + : Math.round(trayBounds.y + trayBounds.height); + } + + trayWindow.setPosition(x, y, false); + trayWindow.show(); + trayWindow.focus(); + } +} + export const initialize = (getAssetPath: (...paths: string[]) => string) => { logger.info('tray initializing...'); _getAssetPath = getAssetPath; + let icon = getAssetPath('icons', 'tray', 'NNIconDefaultInvertedTemplate.png'); if (isWindows()) { icon = getAssetPath('icon.ico'); } tray = new Tray(icon); - // on windows, show a colored icon, 64x64 default icon seems ok - updateTrayMenu(); - // Update the status of everything in the tray when it is opened + if (isMac()) { + createCustomTrayWindow(); + } else { + updateTrayMenu(); + } + tray.on('click', () => { // on windows, default is open/show window on click // on mac, default is open menu on click (no code needed) // on linux? - updateTrayMenu(); + if (isMac()) { + toggleCustomTrayWindow(); + updateCustomTrayMenu(); + } else { + updateTrayMenu(); + } + if (isWindows()) { const window = getMainWindow(); if (window) { @@ -171,8 +459,10 @@ export const initialize = (getAssetPath: (...paths: string[]) => string) => { } } }); + // on windows, the menu opens with a right click (no code needed) // also, the 'right-click' event is not triggered on windows + logger.info('tray initialized'); }; diff --git a/src/renderer/Generics/redesign/Banner/Banner.tsx b/src/renderer/Generics/redesign/Banner/Banner.tsx index 9d3e4e9fe..f5e271d45 100644 --- a/src/renderer/Generics/redesign/Banner/Banner.tsx +++ b/src/renderer/Generics/redesign/Banner/Banner.tsx @@ -45,7 +45,7 @@ export const Banner = ({ const [iconId, setIconId] = useState('blank'); const [title, setTitle] = useState(''); const [loading, setLoading] = useState(false); - const [isClicked, setIsClicked] = useState(false); + // const [isClicked, setIsClicked] = useState(false); const { t: g } = useTranslation('genericComponents'); useEffect(() => { @@ -53,7 +53,7 @@ export const Banner = ({ setIconId('boltstrike'); setTitle(g('CurrentlyOffline')); setDescription(g('PleaseReconnect')); - } else if (!podmanInstalled) { + } else if (podmanInstalled === false) { setIconId('warningcircle'); setTitle(g('PodmanIsNotInstalled')); setDescription(g('ClickToInstallPodman')); @@ -66,14 +66,14 @@ export const Banner = ({ setTitle(g('PodmanIsNotRunning')); setDescription(g('ClickToStartPodman')); } - }, [offline, updateAvailable, podmanStopped, podmanInstalled, g]); + }, [offline, updateAvailable, podmanStopped, podmanInstalled]); const onClickBanner = () => { - if (isClicked || offline) { + if (offline) { return; } - setIsClicked(true); + // setIsClicked(true); if (!podmanInstalled) { setDescription(g('PodmanInstalling')); diff --git a/src/renderer/Generics/redesign/Modal/modal.css.ts b/src/renderer/Generics/redesign/Modal/modal.css.ts index 07aa71157..6e58deb36 100644 --- a/src/renderer/Generics/redesign/Modal/modal.css.ts +++ b/src/renderer/Generics/redesign/Modal/modal.css.ts @@ -44,6 +44,12 @@ export const modalContentStyle = style({ '&.failSystemRequirements': { width: '380px', }, + '&.podman': { + width: '624px', + }, + '&.preferences': { + width: '624px', + }, }, }); diff --git a/src/renderer/Presentational/ModalManager/ModalManager.tsx b/src/renderer/Presentational/ModalManager/ModalManager.tsx index 5ecb5a6c4..d4ba3094e 100644 --- a/src/renderer/Presentational/ModalManager/ModalManager.tsx +++ b/src/renderer/Presentational/ModalManager/ModalManager.tsx @@ -8,6 +8,7 @@ import { AlphaBuildModal } from './AlphaBuildModal'; import { ControllerUpdateModal } from './ControllerUpdateModal.js'; import FailSystemRequirementsModal from './FailSystemRequirementsModal'; import { NodeSettingsModal } from './NodeSettingsModal'; +import { PodmanModal } from './PodmanModal'; import { PreferencesModal } from './PreferencesModal'; import { RemoveNodeModal } from './RemoveNodeModal'; import { ResetConfigModal } from './ResetConfigModal'; @@ -43,6 +44,8 @@ const ModalManager = () => { return ; case modalRoutes.failSystemRequirements: return ; + case modalRoutes.podman: + return ; case modalRoutes.addValidator: return null; case modalRoutes.clientVersions: diff --git a/src/renderer/Presentational/ModalManager/PodmanModal.tsx b/src/renderer/Presentational/ModalManager/PodmanModal.tsx new file mode 100644 index 000000000..874bdaf2c --- /dev/null +++ b/src/renderer/Presentational/ModalManager/PodmanModal.tsx @@ -0,0 +1,59 @@ +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Modal } from '../../Generics/redesign/Modal/Modal.js'; +import electron from '../../electronGlobal.js'; +import { reportEvent } from '../../events/reportEvent.js'; +import PodmanWrapper from '../PodmanModal/PodmanWrapper.js'; +import { type ModalConfig, modalOnChangeConfig } from './modalUtils.js'; + +type Props = { + modalOnClose: () => void; +}; + +export const PodmanModal = ({ modalOnClose }: Props) => { + const [modalConfig, setModalConfig] = useState({}); + const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(false); + const { t } = useTranslation(); + const buttonSaveLabel = t('Done'); + + const modalOnSaveConfig = async (updatedConfig: ModalConfig | undefined) => { + try { + console.log('set some kind of setting here?'); + } catch (err) { + console.error(err); + throw new Error( + 'There was an error removing the node. Try again and please report the error to the NiceNode team in Discord.', + ); + } + modalOnClose(); + }; + + const disableSaveButton = useCallback((value: boolean) => { + setIsSaveButtonDisabled(value); + }, []); + + return ( + + { + modalOnChangeConfig( + config, + modalConfig, + setModalConfig, + save, + modalOnSaveConfig, + ); + }} + disableSaveButton={disableSaveButton} + /> + + ); +}; diff --git a/src/renderer/Presentational/ModalManager/PreferencesModal.tsx b/src/renderer/Presentational/ModalManager/PreferencesModal.tsx index 96215bccc..a53a7d83a 100644 --- a/src/renderer/Presentational/ModalManager/PreferencesModal.tsx +++ b/src/renderer/Presentational/ModalManager/PreferencesModal.tsx @@ -82,6 +82,7 @@ export const PreferencesModal = ({ modalOnClose }: Props) => { return ( void; -}; - -export const AlphaBuildModal = ({ modalOnClose }: Props) => { - const buttonSaveLabel = 'I Understand'; - - const modalOnSaveConfig = async () => { - await electron.getSetHasSeenAlphaModal(true); - console.log('save!'); - modalOnClose(); - }; - - return ( - - - - ); -}; diff --git a/src/renderer/Presentational/ModalManager/modalUtils.tsx b/src/renderer/Presentational/ModalManager/modalUtils.tsx index b5075a19e..280991e68 100644 --- a/src/renderer/Presentational/ModalManager/modalUtils.tsx +++ b/src/renderer/Presentational/ModalManager/modalUtils.tsx @@ -41,7 +41,7 @@ export const modalRoutes = Object.freeze({ updateUnavailable: 'updateUnavailable', failSystemRequirements: 'failSystemRequirements', alphaBuild: 'alphaBuild', - updatePodman: 'updatePodman', + podman: 'podman', }); /* Use this to change config settings, saved temporarily in the modal file with backend apis until it's saved by modalOnSaveConfig diff --git a/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx b/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx index cb330b75e..6267bbbfe 100644 --- a/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx +++ b/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx @@ -14,6 +14,23 @@ import { bytesToGB } from '../../utils'; import type { NodeRequirementsProps } from './NodeRequirements'; import { findSystemStorageDetailsAtALocation } from './nodeStorageUtil'; +const isVersionHigher = (currentVersion: string, targetVersion: string) => { + const parseVersion = (version: string) => version.split('.').map(Number); + + const current = parseVersion(currentVersion); + const target = parseVersion(targetVersion); + + for (let i = 0; i < Math.max(current.length, target.length); i++) { + const currentPart = current[i] || 0; + const targetPart = target[i] || 0; + if (currentPart > targetPart) return true; + if (currentPart < targetPart) return false; + } + return false; +}; + +const TARGET_MACOS_VERSION = '13.0.0'; + export const makeCheckList = ( { nodeRequirements, systemData, nodeStorageLocation }: NodeRequirementsProps, t: TFunction, @@ -32,6 +49,23 @@ export const makeCheckList = ( } console.log('nodeLocationStorageDetails', nodeLocationStorageDetails); + if (systemData?.os?.platform === 'darwin') { + const checkListItem: ChecklistItemProps = { + checkTitle: t('macOSTitle', { + minVersion: TARGET_MACOS_VERSION, + }), + valueText: t('macOSDescription', { + version: systemData?.os?.release, + }), + status: '', + }; + if (isVersionHigher(systemData?.os?.release, TARGET_MACOS_VERSION)) { + checkListItem.status = 'complete'; + } else { + checkListItem.status = 'error'; + } + newChecklistItems.push(checkListItem); + } for (const [nodeReqKey, nodeReqValue] of Object.entries(nodeRequirements)) { console.log(`${nodeReqKey}: ${nodeReqValue}`); if (nodeReqKey === 'documentationUrl' || nodeReqKey === 'description') { diff --git a/src/renderer/Presentational/PodmanInstallation/PodmanInstallation.tsx b/src/renderer/Presentational/PodmanInstallation/PodmanInstallation.tsx index 0ecdf58ae..f8b997a79 100644 --- a/src/renderer/Presentational/PodmanInstallation/PodmanInstallation.tsx +++ b/src/renderer/Presentational/PodmanInstallation/PodmanInstallation.tsx @@ -31,6 +31,7 @@ import { learnMore, titleFont, } from './podmanInstallation.css'; +import { messageContainer } from './podmanInstallation.css.js'; // 6.5(docker), ? min on 2022 MacbookPro 16inch, baseline const TOTAL_INSTALL_TIME_SEC = 5 * 60; @@ -189,9 +190,6 @@ const PodmanInstallation = ({ // react-hooks/exhaustive-deps // }, []); - console.log('isPodmanInstalled', isPodmanInstalled); - console.log('podmanDetails', podmanDetails); - // listen to podman install messages return (
@@ -200,21 +198,24 @@ const PodmanInstallation = ({ {t('PodmanInstallation')}
)} + {podmanDetails?.isOutdated && ( +
+ +
+ )}
{t('podmanPurpose')}
{/* Podman is not installed */}
- {podmanDetails?.isOutdated && ( -
- -
- )} {(!isPodmanInstalled || podmanDetails?.isOutdated) && ( <> {!sDownloadComplete && !sInstallComplete && ( @@ -224,7 +225,11 @@ const PodmanInstallation = ({