diff --git a/CHANGELOG.md b/CHANGELOG.md index fb5b637..9ac382a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ # Changelog # +## 1.0.2 ## +* Option to launch application hidden (notification icon only) +* Close window to notification area +* Unmount on application exit +* Ability to cancel mount retry on unexpected failure +* OS X support +* SiaPrime support +* Partial Linux support +* Electron to v4 +* Prevent mount if dependencies are missing + ## 1.0.1 ## * Added configuration settings for Repertory 1.0.0-alpha.2 and above * Fixed memory leak on component unmount diff --git a/README.md b/README.md index b4598cb..581f9b5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Repertory UI -![alt text](https://image.ibb.co/mnhA1z/repertory_1_0_1.png) +![alt text](https://image.ibb.co/edOg5A/repertory-ui-1-0-2.png) ### GUI for [Repertory](https://bitbucket.org/blockstorage/repertory) ### -Repertory allows you to mount Hyperspace or Sia blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows. +Repertory allows you to mount Sia, SiaPrime and/or Hyperspace blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows. # Downloads # -* [Repertory UI v1.0.1 Windows 64-bit](https://sia.pixeldrain.com/api/file/Alo1IF1u) +* [Repertory UI v1.0.2 OS X](https://pixeldrain.com/u/OtxPlbOI) +* [Repertory UI v1.0.2 Windows 64-bit](https://pixeldrain.com/u/4oJeVntd) # Supported Platforms # +* OS X * Windows 64-bit # Future Platforms # -* OS X * Linux 64-bit \ No newline at end of file diff --git a/electron.js b/electron.js index 1cbcd7b..faf0556 100644 --- a/electron.js +++ b/electron.js @@ -1,6 +1,6 @@ // Modules to control application life and create native browser window -const {app, BrowserWindow, Tray, nativeImage, Menu} = require('electron'); +const {app, BrowserWindow, Tray, nativeImage, Menu, dialog} = require('electron'); const {ipcMain} = require('electron'); const Constants = require('./src/constants'); const path = require('path'); @@ -14,145 +14,298 @@ const AutoLaunch = require('auto-launch'); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -let mainContextWindow; +let trayContextMenu; let mainWindow; let mainWindowTray; -let mountedPIDs = []; +let mountedData = {}; +let mountedLocations = []; +let expectedUnmount = {}; +let launchHidden = false; +let firstMountCheck = true; +let manualMountDetection = {}; + +let isQuiting = false; +app.on('before-quit', function () { + isQuiting = true; +}); + +function closeApplication() { + app.quit(); +} + +function setWindowVisibility(show) { + if (show) { + mainWindow.show(); + if (os.platform() === 'darwin') { + app.dock.show(); + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + + mainWindow.focus(); + } else { + mainWindow.hide(); + if (os.platform() === 'darwin') { + app.dock.hide(); + } + } + + if (trayContextMenu && mainWindowTray) { + trayContextMenu.items[0].checked = show; + mainWindowTray.setContextMenu(trayContextMenu) + } +} function createWindow() { + loadUiSettings(); + // Create the browser window. - const height = process.env.ELECTRON_START_URL ? 324 : 304; + const height = (process.env.ELECTRON_START_URL || (os.platform() === 'darwin') ? 364 : 344) - ((os.platform() === 'win32') ? 0 : 20); mainWindow = new BrowserWindow({ - width: 425, + width: 428 + ((os.platform() === 'win32') ? 0 : (os.platform() === 'darwin') ? 150 : 160), height: height, + fullscreen: false, resizable: false, + show: !launchHidden, title: 'Repertory UI', webPreferences: { webSecurity: !process.env.ELECTRON_START_URL } }); + if ((os.platform() === 'darwin') && launchHidden) { + app.dock.hide(); + } + // and load the index.html of the app. const startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '/build/index.html'), protocol: 'file:', slashes: true }); - mainWindow.loadURL(startUrl); - // Emitted when the window is closed. + mainWindow.on('close', function (event) { + if (!isQuiting) { + event.preventDefault(); + if (mainWindow.isVisible()) { + setWindowVisibility(false); + } + event.returnValue = false; + } + }); + mainWindow.on('closed', function () { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. - mainWindow = null + mainWindow = null; + + // Unmount all items + for (const i in mountedLocations) { + const data = mountedData[mountedLocations[i]]; + helpers.stopMountProcessSync(data.DataDirectory, data.Version, data.StorageType); + } + + mountedLocations = []; + mountedData = {}; }); - if ((os.platform() === 'win32') || (os.platform() === 'linux')) { - const appPath = (os.platform() === 'win32') ? - path.resolve(path.join(app.getAppPath(), '..\\..\\repertory-ui.exe')) : - process.env.APPIMAGE; + const appPath = (os.platform() === 'win32') ? path.resolve(path.join(app.getAppPath(), '..\\..\\repertory-ui.exe')) : + (os.platform() === 'darwin') ? path.resolve(path.join(path.dirname(app.getAppPath()), '../MacOS/repertory-ui')) : + process.env.APPIMAGE; - const autoLauncher = new AutoLaunch({ - name: 'Repertory UI', - path: appPath, - }); + const autoLauncher = new AutoLaunch({ + name: 'Repertory UI', + path: appPath, + }); - const image = nativeImage.createFromPath(path.join(__dirname, '/build/logo.png')); - mainContextWindow = Menu.buildFromTemplate([ - { - label: 'Visible', type: 'checkbox', click(item) { - if (item.checked) { - mainWindow.show(); - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } - mainWindow.focus() - } else { - mainWindow.hide(); - } - } + trayContextMenu = Menu.buildFromTemplate([ + { + label: 'Visible', type: 'checkbox', click(item) { + setWindowVisibility(item.checked); }, - { - label: 'Auto-start', type: 'checkbox', click(item) { - if (item.checked) { - autoLauncher.enable(); - } else { - autoLauncher.disable(); - } - } - }, - { - type: 'separator' - }, - { - label: 'Exit', click(item) { - app.quit(); + checked: !launchHidden, + }, + { + label: 'Auto-start', type: 'checkbox', click(item) { + if (item.checked) { + autoLauncher.enable(); + } else { + autoLauncher.disable(); } } - ]); + }, + { + type: 'separator' + }, + { + label: 'Launch Hidden', type: 'checkbox', click(item) { + launchHidden = !!item.checked; + saveUiSettings(); + }, + checked: launchHidden, + }, + { + type: 'separator' + }, + { + label: 'Exit and Unmount', click() { + closeApplication(); + } + } + ]); - mainContextWindow.items[0].checked = true; - autoLauncher - .isEnabled() - .then((enabled) => { - mainContextWindow.items[1].checked = enabled; + const image = nativeImage.createFromPath(path.join(__dirname, (os.platform() === 'darwin') ? '/build/logo_mac.png' : '/build/logo.png')); + mainWindowTray = new Tray(image); - mainWindowTray = new Tray(image); - mainWindowTray.setToolTip('Repertory UI'); - mainWindowTray.setContextMenu(mainContextWindow) - }) - .catch(() => { - app.quit(); - }); - } + autoLauncher + .isEnabled() + .then((enabled) => { + trayContextMenu.items[1].checked = enabled; + mainWindowTray.setToolTip('Repertory UI'); + mainWindowTray.setContextMenu(trayContextMenu) + }) + .catch(() => { + closeApplication(); + }); + + mainWindow.loadURL(startUrl); } const instanceLock = app.requestSingleInstanceLock(); if (!instanceLock) { - app.quit() + closeApplication(); } else { app.on('second-instance', () => { if (mainWindow) { - mainWindow.show(); - if (mainContextWindow) { - mainContextWindow.items[0].checked = true; - } - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } - mainWindow.focus() + setWindowVisibility(true); } }); app.on('ready', createWindow); app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit() - } - }); - - app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (mainWindow === null) { - createWindow() - } + closeApplication(); }); } +const clearManualMountDetection = (storageType) => { + if (manualMountDetection[storageType]) { + clearInterval(manualMountDetection[storageType]); + delete manualMountDetection[storageType]; + } +}; + +const loadUiSettings = () => { + const settingFile = path.join(helpers.resolvePath(Constants.DATA_LOCATIONS[os.platform()]), 'ui.json'); + try { + if (fs.statSync(settingFile).isFile()) { + const settings = JSON.parse(fs.readFileSync(settingFile, 'utf8')); + launchHidden = settings.launch_hidden; + } + } catch (e) { + } +}; + +const monitorMount = (sender, storageType, dataDirectory, version, pid, location) => { + manualMountDetection[storageType] = setInterval(() => { + helpers + .detectRepertoryMounts(dataDirectory, version) + .then(result => { + if (result[storageType].PID !== pid) { + if (result[storageType].PID === -1) { + clearManualMountDetection(storageType); + sender.send(Constants.IPC_Unmount_Drive_Reply, { + data: { + Expected: expectedUnmount[storageType], + Location: location, + StorageType: storageType, + Error: Error(storageType + ' Unmounted').toString(), + Success: false, + } + }); + } else { + pid = result[storageType].PID; + } + } + }) + .catch(e => { + console.log(e); + }); + },6000); +}; + +const saveUiSettings = () => { + const settingFile = path.join(helpers.resolvePath(Constants.DATA_LOCATIONS[os.platform()]), 'ui.json'); + try { + fs.writeFileSync(settingFile, JSON.stringify({ + launch_hidden: launchHidden, + }), 'utf-8'); + } catch (e) { + } +}; + const standardIPCReply = (event, channel, data, error) => { - event.sender.send(channel, { - data: { - ...data, - Error: error, - Success: !error, + if (mainWindow) { + event.sender.send(channel, { + data: { + ...data, + Error: error instanceof Error ? error.toString() : error, + Success: !error, + } + }); + } +}; + +ipcMain.on(Constants.IPC_Browse_Directory + '_sync', (event, data) => { + dialog.showOpenDialog(mainWindow, { + defaultPath: data.Location, + properties: ['openDirectory'], + title: data.Title, + }, (filePaths) => { + if (filePaths && (filePaths.length > 0)) { + event.returnValue = filePaths[0]; + } else { + event.returnValue = ''; } }); -}; +}); + +ipcMain.on(Constants.IPC_Check_Dependency_Installed, (event, data) => { + try { + const exists = fs.lstatSync(data.File).isFile(); + standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, { + data: { + Exists: exists, + }, + }); + } catch (e) { + standardIPCReply(event, Constants.IPC_Check_Dependency_Installed_Reply, { + data : { + Exists: false, + }, + }); + } +}); + +ipcMain.on(Constants.IPC_Check_Dependency_Installed + '_sync', (event, data) => { + try { + const ls = fs.lstatSync(data.File); + event.returnValue = { + data: { + Exists: ls.isFile() || ls.isSymbolicLink(), + }, + }; + } catch (e) { + event.returnValue = { + data: { + Exists: false + }, + }; + } +}); ipcMain.on(Constants.IPC_Check_Installed, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); @@ -178,6 +331,29 @@ ipcMain.on(Constants.IPC_Check_Installed, (event, data) => { }); }); +ipcMain.on(Constants.IPC_Check_Mount_Location + '_sync', (event, data) => { + let response = { + Success: true, + Error: '' + }; + + try { + if (fs.existsSync(data.Location) && fs.statSync(data.Location).isDirectory()) { + if (fs.readdirSync(data.Location).length !== 0) { + response.Success = false; + response.Error = 'Directory not empty: ' + data.Location; + } + } else { + response.Success = false; + response.Error = 'Directory not found: ' + data.Location; + } + } catch (e) { + response.Success = false; + response.Error = e.toString(); + } + event.returnValue = response; +}); + ipcMain.on(Constants.IPC_Delete_File, (event, data) => { try { if (fs.existsSync(data.FilePath)) { @@ -187,87 +363,123 @@ ipcMain.on(Constants.IPC_Delete_File, (event, data) => { } }); -ipcMain.on(Constants.IPC_Detect_Mounts, (event, data) => { - let driveLetters = { - Hyperspace: [], - Sia: [], - }; +ipcMain.on(Constants.IPC_Delete_File + '_sync', (event, data) => { + try { + if (fs.existsSync(data.FilePath)) { + fs.unlinkSync(data.FilePath); + } + event.returnValue = { + data: true, + }; + } catch (e) { + event.returnValue = { + data: false, + }; + } +}); - const grabDriveLetters = (hsLocation, siaLocation) => { +ipcMain.on(Constants.IPC_Detect_Mounts, (event, data) => { + let driveLetters = {}; + for (const provider of Constants.PROVIDER_LIST) { + driveLetters[provider] = []; + } + + const grabDriveLetters = (locations) => { for (let i = 'c'.charCodeAt(0); i <= 'z'.charCodeAt(0); i++) { const drive = (String.fromCharCode(i) + ':').toUpperCase(); - if (!(hsLocation.startsWith(drive) || siaLocation.startsWith(drive))) { + let driveInUse; + if (Object.keys(locations).length > 0) { + for (const provider of Constants.PROVIDER_LIST) { + driveInUse = locations[provider].startsWith(drive); + if (driveInUse) + break; + } + } + if (!driveInUse) { try { if (!fs.existsSync(drive)) { - driveLetters.Hyperspace.push(drive); - driveLetters.Sia.push(drive); + for (const provider of Constants.PROVIDER_LIST) { + driveLetters[provider].push(drive); + } } } catch (e) { } } } - if (hsLocation.length > 0) { - if (!driveLetters.Hyperspace.find((driveLetter) => { - return driveLetter === hsLocation; - })) { - driveLetters.Hyperspace.push(hsLocation); - } - } - - if (siaLocation.length > 0) { - if (!driveLetters.Sia.find((driveLetter) => { - return driveLetter === siaLocation; - })) { - driveLetters.Sia.push(siaLocation); + if (Object.keys(locations).length > 0) { + for (const provider of Constants.PROVIDER_LIST) { + if (locations[provider].length > 0) { + if (!driveLetters[provider].find((driveLetter) => { + return driveLetter === locations[provider]; + })) { + driveLetters[provider].push(locations[provider]); + } + } } } }; - const setImage = (hsLocation, siaLocation) => { - if (os.platform() === 'win32') { - let image; - if ((siaLocation.length > 0) && (hsLocation.length > 0)) { - image = nativeImage.createFromPath(path.join(__dirname, '/build/logo_both.png')); - } else if (hsLocation.length > 0) { - image = nativeImage.createFromPath(path.join(__dirname, '/build/logo_hs.png')); - } else if (siaLocation.length > 0) { - image = nativeImage.createFromPath(path.join(__dirname, '/build/logo_sia.png')); - } else { - image = nativeImage.createFromPath(path.join(__dirname, '/build/logo.png')); + const setImage = (locations) => { + let driveInUse; + if (Object.keys(locations).length > 0) { + for (const provider of Constants.PROVIDER_LIST) { + driveInUse = locations[provider].length > 0; + if (driveInUse) + break; } - - mainWindowTray.setImage(image); } + + let image; + if (driveInUse) { + image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '/build/logo_both_mac.png' : '/build/logo_both.png')); + } else { + image = nativeImage.createFromPath(path.join(__dirname, os.platform() === 'darwin' ? '/build/logo_mac.png' : '/build/logo.png')); + } + + mainWindowTray.setImage(image); }; const dataDirectory = helpers.resolvePath(data.Directory); helpers .detectRepertoryMounts(dataDirectory, data.Version) .then((results) => { - let hsLocation = results.Hyperspace.Location; - let siaLocation = results.Sia.Location; - if (os.platform() === 'win32') { - hsLocation = hsLocation.toUpperCase(); - siaLocation = siaLocation.toUpperCase(); - grabDriveLetters(hsLocation, siaLocation); + let storageData = {}; + let locations = {}; + for (const provider of Constants.PROVIDER_LIST) { + storageData[provider] = results[provider] ? results[provider] : { + Active: false, + Location: '', + PID: -1, + }; + locations[provider] = storageData[provider].Location; + + if (storageData[provider].PID !== -1) { + expectedUnmount[provider] = false; + if (firstMountCheck) { + monitorMount(event.sender, provider, dataDirectory, data.Version, storageData[provider].PID, storageData[provider].Location); + } + } + } + + if (os.platform() === 'win32') { + grabDriveLetters(locations); + } + + setImage(locations); + if (firstMountCheck) { + firstMountCheck = false; } - setImage(hsLocation, siaLocation); standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, { DriveLetters: driveLetters, - Locations: { - Hyperspace: hsLocation, - Sia: siaLocation, - }, - PIDS: { - Hyperspace: results.Hyperspace.PID, - Sia: results.Sia.PID, - } + Locations: locations, }); }) .catch(error => { - grabDriveLetters('', ''); - setImage('', ''); + if (os.platform() === 'win32') { + grabDriveLetters({}); + } + setImage({}); standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, { DriveLetters: driveLetters, }, error); @@ -379,57 +591,119 @@ ipcMain.on(Constants.IPC_Grab_UI_Releases, (event) => { }); ipcMain.on(Constants.IPC_Install_Dependency, (event, data) => { - helpers + if (data.Source.toLowerCase().endsWith('.dmg')) { + helpers + .executeAsync('open', ['-a', 'Finder', '-W', data.Source]) + .then(() => { + standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { + Source: data.Source, + URL: data.URL, + }); + }) + .catch(error=> { + standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { + Source: data.Source, + URL: data.URL, + }, error); + }); + } else { + helpers .executeAndWait(data.Source) - .then(()=> { + .then(() => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, + URL: data.URL, }); }) .catch(error => { standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { Source: data.Source, + URL: data.URL, }, error); }); + } }); ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => { - helpers - .executeAsync(data.Source) - .then(()=> { - mainWindow.close(); - }) - .catch(error => { - standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { - Source: data.Source, - }, error); - }); + if (os.platform() === 'win32') { + helpers + .executeAsync(data.Source) + .then(() => { + closeApplication(); + }) + .catch(error => { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, error); + }); + } else if (data.Source.toLocaleLowerCase().endsWith('.dmg')) { + helpers + .executeAsync('open', ['-a', 'Finder', data.Source]) + .then(() => { + closeApplication(); + }) + .catch(error => { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, error); + }); + } else if (data.Source.toLocaleLowerCase().endsWith('.appimage')) { + // TODO Generate and execute script with delay + /*helpers + .executeAsync(data.Source) + .then(() => { + closeApplication(); + }) + .catch(error => { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, error); + });*/ + } else { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, Error('Unsupported upgrade: ' + data.Source)); + } }); ipcMain.on(Constants.IPC_Mount_Drive, (event, data) => { + expectedUnmount[data.StorageType] = false; const dataDirectory = helpers.resolvePath(data.Directory); - const errorHandler = (pid, error) => { - mountedPIDs.splice(mountedPIDs.indexOf(pid), 1); - standardIPCReply(event, Constants.IPC_Unmount_Drive_Reply, { - PID: -1, + + if (mountedLocations.indexOf(data.Location) !== -1) { + console.log(data.StorageType + ' already mounted: ' + data.Location); + } else { + mountedLocations.push(data.Location); + mountedData[data.Location] = { + DataDirectory: dataDirectory, + Version: data.Version, StorageType: data.StorageType, - }, error || Error(data.StorageType + ' Unmounted')); - }; - helpers.executeMount(dataDirectory, data.Version, data.StorageType, data.Location, (error, pid) => { - errorHandler(pid, error); - }) - .then(pid => { - if (pid !== -1) { - mountedPIDs.push(pid); - } - standardIPCReply(event, Constants.IPC_Mount_Drive_Reply, { - PID: pid, - StorageType: data.StorageType, - }); - }) - .catch(error => { - errorHandler(-1, error); - }); + }; + const errorHandler = (pid, error) => { + if (mountedLocations.indexOf(data.Location) !== -1) { + mountedLocations.splice(mountedLocations.indexOf(data.Location), 1); + delete mountedData[data.Location]; + } + + standardIPCReply(event, Constants.IPC_Unmount_Drive_Reply, { + Expected: expectedUnmount[data.StorageType], + Location: data.Location, + StorageType: data.StorageType, + }, error || Error(data.StorageType + ' Unmounted')); + }; + helpers + .executeMount(dataDirectory, data.Version, data.StorageType, data.Location, data.NoConsoleSupported, (error, pid) => { + errorHandler(pid, error); + }) + .then(() => { + standardIPCReply(event, Constants.IPC_Mount_Drive_Reply, { + StorageType: data.StorageType, + }); + }) + .catch(error => { + errorHandler(-1, error); + }); + } }); ipcMain.on(Constants.IPC_Save_State, (event, data) => { @@ -459,16 +733,18 @@ ipcMain.on(Constants.IPC_Set_Config_Values, (event, data) => { }); ipcMain.on(Constants.IPC_Shutdown, () => { - app.quit(); + closeApplication(); }); ipcMain.on(Constants.IPC_Unmount_Drive, (event, data) => { + clearManualMountDetection(data.StorageType); + + const dataDirectory = helpers.resolvePath(data.Directory); + expectedUnmount[data.StorageType] = true; helpers - .stopProcessByPID(data.PID) - .then((pid)=> { - if (mountedPIDs.indexOf(pid) === -1) { - event.sender.send(Constants.IPC_Unmount_Drive_Reply); - } + .stopMountProcess(dataDirectory, data.Version, data.StorageType) + .then((result)=> { + console.log(result); }) .catch((e) => { console.log(e); diff --git a/helpers.js b/helpers.js index c88b27e..e494960 100644 --- a/helpers.js +++ b/helpers.js @@ -4,6 +4,7 @@ const os = require('os'); const axios = require('axios'); const exec = require('child_process').exec; const spawn = require('child_process').spawn; +const Constants = require('./src/constants'); const tryParse = (j, def) => { try { @@ -37,18 +38,15 @@ module.exports.detectRepertoryMounts = (directory, version) => { }); process.on('exit', () => { - resolve(tryParse(result, { - Hyperspace: { + let defaultData = {}; + for (const provider of Constants.PROVIDER_LIST) { + defaultData[provider] = { Active: false, Location: '', PID: -1, - }, - Sia: { - Active: false, - Location: '', - PID: -1, - }, - })); + }; + } + resolve(tryParse(result, defaultData)); }); process.unref(); }); @@ -120,15 +118,15 @@ module.exports.executeAndWait = command => { }); }; -module.exports.executeAsync = (command) => { +module.exports.executeAsync = (command, args=[]) => { return new Promise((resolve, reject) => { const launchProcess = (count, timeout) => { const processOptions = { detached: true, - shell: true, + shell: false, }; - const process = new spawn(command, [], processOptions); + const process = new spawn(command, args, processOptions); const pid = process.pid; process.on('error', (err) => { @@ -136,15 +134,18 @@ module.exports.executeAsync = (command) => { reject(err, pid); } else { clearTimeout(timeout); - setTimeout(()=>launchProcess(count, setTimeout(() => resolve(), 3000)), 1000); + setTimeout(()=> launchProcess(count, setTimeout(() => resolve(), 3000)), 1000); } }); process.on('exit', (code) => { - if (++count === 5) { - reject(code, pid); - } else { - setTimeout(()=>launchProcess(count, setTimeout(() => resolve(), 3000)), 1000); + if (code !== 0) { + if (++count === 5) { + reject(code, pid); + } else { + clearTimeout(timeout); + setTimeout(() => launchProcess(count, setTimeout(() => resolve(), 3000)), 1000); + } } }); @@ -155,28 +156,33 @@ module.exports.executeAsync = (command) => { }); }; -module.exports.executeMount = (directory, version, storageType, location, exitCallback) => { +module.exports.executeMount = (directory, version, storageType, location, noConsoleSupported, exitCallback) => { return new Promise((resolve) => { const processOptions = { - detached: true, - shell: true, + detached: false, + shell: os.platform() !== 'darwin', + stdio: 'ignore', }; const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); const args = []; - if (storageType.toLowerCase() === 'hyperspace') { - args.push('-hs'); + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); } - if (os.platform() === 'linux') { - args.push("-o"); - args.push("big_writes"); - } - if (os.platform() === 'win32') { + + if ((os.platform() === 'linux') || (os.platform() === 'darwin')) { + args.push('-o'); + args.push('big_writes'); + args.push('-f'); + if (noConsoleSupported) { + args.push('-nc'); + } + } else if (os.platform() === 'win32') { args.push('-hidden'); } args.push(location); - const process = new spawn(command, args, processOptions); + let process = new spawn(command, args, processOptions); const pid = process.pid; const timeout = setTimeout(() => { @@ -187,11 +193,11 @@ module.exports.executeMount = (directory, version, storageType, location, exitCa clearTimeout(timeout); exitCallback(err, pid); }); + process.on('exit', (code) => { clearTimeout(timeout); exitCallback(code, pid); }); - process.unref(); }); }; @@ -206,8 +212,8 @@ module.exports.getConfig = (directory, version, storageType) => { const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); const args = []; args.push('-dc'); - if (storageType.toLowerCase() === 'hyperspace') { - args.push('-hs'); + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); } const process = new spawn(command, args, processOptions); @@ -252,8 +258,8 @@ module.exports.getConfigTemplate = (directory, version, storageType) => { const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); const args = []; args.push('-gt'); - if (storageType.toLowerCase() === 'hyperspace') { - args.push('-hs'); + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); } const process = new spawn(command, args, processOptions); @@ -276,8 +282,8 @@ module.exports.getConfigTemplate = (directory, version, storageType) => { module.exports.getMissingDependencies = dependencies => { return new Promise((resolve, reject) => { - if (!dependencies || (dependencies.length === 0)) { - reject(Error('Dependency list is empty')); + if (!dependencies) { + reject(Error('Dependency list is invalid')); } let missing = []; @@ -334,7 +340,7 @@ module.exports.getMissingDependencies = dependencies => { } else { for (const dep of dependencies) { try { - if (!fs.lstatSync(dep.file).isFile()) { + if (!(fs.lstatSync(dep.file).isFile() || fs.lstatSync(dep.file).isSymbolicLink())) { missing.push(dep); } } catch (e) { @@ -415,8 +421,8 @@ module.exports.setConfigValue = (name, value, directory, storageType, version) = args.push('-set'); args.push(name); args.push(value); - if (storageType.toLowerCase() === 'hyperspace') { - args.push('-hs'); + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); } const process = new spawn(command, args, processOptions); @@ -433,27 +439,51 @@ module.exports.setConfigValue = (name, value, directory, storageType, version) = }); }; -module.exports.stopProcessByPID = pid => { +module.exports.stopMountProcess = (directory, version, storageType) => { return new Promise((resolve, reject) => { const processOptions = { - detached: true, - shell: false, + detached: os.platform() === 'darwin', + shell: os.platform() !== 'darwin', windowsHide: true, }; - const command = (os.platform() === 'win32') ? 'taskkill.exe' : ''; - const args = []; - args.push('/PID'); - args.push(pid); + const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); + const args = ['-unmount']; + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); + } const process = new spawn(command, args, processOptions); - + const pid = process.pid; process.on('error', (err) => { reject(err); }); - process.on('exit', () => { - setTimeout(()=>resolve(pid), 3000); + process.on('exit', (code) => { + resolve({ + PID: pid, + Code: code, + }); }); - process.unref(); + + if (os.platform() === 'darwin') { + process.unref(); + } }); +}; + +module.exports.stopMountProcessSync = (directory, version, storageType) => { + const processOptions = { + detached: true, + shell: os.platform() !== 'darwin', + windowsHide: true, + }; + + const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); + const args = ['-unmount']; + if (Constants.PROVIDER_ARG[storageType.toLowerCase()].length > 0) { + args.push(Constants.PROVIDER_ARG[storageType.toLowerCase()]); + } + + const process = new spawn(command, args, processOptions); + process.unref(); }; \ No newline at end of file diff --git a/package.json b/package.json index 85f6bfd..9a7d2a5 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "repertory-ui", - "version": "1.0.1", + "version": "1.0.2", "private": true, + "author": "scott.e.graves@gmail.com", + "description": "GUI for Repertory - Repertory allows you to mount Hyperspace, Sia and/or SiaPrime blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows.", + "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.1", - "@fortawesome/free-solid-svg-icons": "^5.1.1", + "@fortawesome/fontawesome-svg-core": "^1.2.10", + "@fortawesome/free-solid-svg-icons": "^5.6.1", "@fortawesome/react-fontawesome": "^0.1.0", "auto-launch": "^5.0.5", "autoprefixer": "7.1.6", @@ -17,12 +20,12 @@ "babel-runtime": "6.26.0", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", - "color": "^3.0.0", + "color": "^3.1.0", "color-string": "^1.5.2", "css-loader": "0.28.7", "dotenv": "4.0.0", "dotenv-expand": "4.2.0", - "electron-debug": "^2.0.0", + "electron-debug": "^2.1.0", "eslint": "4.10.0", "eslint-config-react-app": "^2.1.0", "eslint-loader": "1.9.0", @@ -35,23 +38,23 @@ "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", "jest": "20.0.4", - "node-schedule": "^1.3.0", - "npm": "^6.2.0", + "node-schedule": "^1.3.1", + "npm": "^6.6.0", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.8", "promise": "8.0.1", "raf": "3.4.0", - "react": "^16.4.1", - "react-css-modules": "^4.7.4", - "react-dev-utils": "^5.0.1", - "react-dom": "^16.4.1", - "react-loader-spinner": "^2.0.6", - "react-tooltip": "^3.8.4", + "react": "^16.6.1", + "react-css-modules": "^4.7.8", + "react-dev-utils": "^5.0.3", + "react-dom": "^16.6.1", + "react-loader-spinner": "^2.3.0", + "react-tooltip": "^3.9.0", "resolve": "1.6.0", "style-loader": "0.19.0", "sw-precache-webpack-plugin": "0.11.4", - "unzipper": "^0.9.3", + "unzipper": "^0.9.6", "url-loader": "0.6.2", "webpack": "3.8.1", "webpack-dev-server": "2.9.4", @@ -63,14 +66,20 @@ "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js --env=jsdom", - "electron-dev": "cross-env ELECTRON_START_URL=http://localhost:3000 electron .", - "pack": "npm run build && electron-builder --dir", - "dist": "npm run build && electron-builder" + "electron-dev": "cross-env ELECTRON_START_URL=http://localhost:3000 electron %NODE_DEBUG_OPTION% .", + "electron-dev-unix": "cross-env ELECTRON_START_URL=http://localhost:3000 electron $NODE_DEBUG_OPTION .", + "pack": "npm run build && electron-builder --dir --x64", + "dist": "npm run build && electron-builder --x64", + "dist-all": "npm run build && electron-builder --x64 --win --linux --mac", + "dist-mac": "npm run build && electron-builder --x64 --mac", + "dist-linux": "npm run build && electron-builder --x64 --linux", + "dist-win": "npm run build && electron-builder --x64 --win", + "postinstall": "electron-builder install-app-deps" }, "devDependencies": { "cross-env": "^5.2.0", - "electron": "^3.0.2", - "electron-builder": "^20.28.4", + "electron": "^4.1.0", + "electron-builder": "^20.38.5", "extract-text-webpack-plugin": "^3.0.2", "webpack-browser-plugin": "^1.0.20" }, @@ -120,6 +129,7 @@ "homepage": "./", "build": { "appId": "repertory-ui", + "artifactName": "${productName}_${version}_${os}_${arch}.${ext}", "files": [ "./electron.js", "./src/constants.js", @@ -128,10 +138,19 @@ "./helpers.js" ], "linux": { - "icon": "./build/icon.icns" + "category": "Utility", + "icon": "./build/logo.png", + "target": "AppImage" + }, + "mac": { + "category": "public.app-category.utilities", + "icon": "./build/icon_color.icns", + "target": "dmg", + "darkModeSupport": true }, "win": { - "icon": "./build/icon.ico" + "icon": "./build/icon.ico", + "target": "nsis" }, "directories": { "buildResources": "public" diff --git a/public/favicon_old.ico b/public/favicon_old.ico deleted file mode 100644 index 530403f..0000000 Binary files a/public/favicon_old.ico and /dev/null differ diff --git a/public/icon_color.icns b/public/icon_color.icns new file mode 100644 index 0000000..6f2482b Binary files /dev/null and b/public/icon_color.icns differ diff --git a/public/icon_old.ico b/public/icon_old.ico deleted file mode 100644 index 391e4a8..0000000 Binary files a/public/icon_old.ico and /dev/null differ diff --git a/public/logo.xcf b/public/logo.xcf new file mode 100644 index 0000000..3b73421 Binary files /dev/null and b/public/logo.xcf differ diff --git a/public/logo_both_mac.png b/public/logo_both_mac.png new file mode 100644 index 0000000..849bbb1 Binary files /dev/null and b/public/logo_both_mac.png differ diff --git a/public/logo_hs.png b/public/logo_hs.png deleted file mode 100644 index a7ce38c..0000000 Binary files a/public/logo_hs.png and /dev/null differ diff --git a/public/logo_mac.png b/public/logo_mac.png new file mode 100644 index 0000000..0836d8e Binary files /dev/null and b/public/logo_mac.png differ diff --git a/public/logo_sia.png b/public/logo_sia.png deleted file mode 100644 index bf5c63d..0000000 Binary files a/public/logo_sia.png and /dev/null differ diff --git a/releases.json b/releases.json index 20714ed..7f41b9f 100644 --- a/releases.json +++ b/releases.json @@ -1,24 +1,24 @@ { "Locations": { "win32": { - "1.0.1": { + "1.0.2": { "hash": "", - "urls": [ - "https://sia.pixeldrain.com/api/file/Alo1IF1u" - ] - }, - "1.0.0": { + "urls": ["https://pixeldrain.com/u/4oJeVntd"] + } + }, + "darwin": { + "1.0.2": { "hash": "", - "urls": [ - "https://sia.pixeldrain.com/api/file/qXM6pVZZ" - ] + "urls": ["https://pixeldrain.com/u/OtxPlbOI"] } } }, "Versions": { "win32": [ - "1.0.1", - "1.0.0" + "1.0.2" + ], + "darwin": [ + "1.0.2" ] } } \ No newline at end of file diff --git a/src/App.js b/src/App.js index fe29cf7..a169f40 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React from 'react'; import axios from 'axios'; import styles from './App.css'; import Box from './components/UI/Box/Box'; @@ -15,33 +15,35 @@ import ReleaseVersionDisplay from './components/ReleaseVersionDisplay/ReleaseVer import Text from './components/UI/Text/Text'; import UpgradeIcon from './components/UpgradeIcon/UpgradeIcon'; import UpgradeUI from './components/UpgradeUI/UpgradeUI'; +import IPCContainer from './containers/IPCContainer/IPCContainer'; const Constants = require('./constants'); const Scheduler = require('node-schedule'); -let ipcRenderer = null; -if (!process.versions.hasOwnProperty('electron')) { - ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); -} - -class App extends Component { +class App extends IPCContainer { constructor(props) { super(props); - if (ipcRenderer) { - ipcRenderer.on(Constants.IPC_Check_Installed_Reply, this.onCheckInstalledReply); - ipcRenderer.on(Constants.IPC_Download_File_Complete, this.onDownloadFileComplete); - ipcRenderer.on(Constants.IPC_Download_File_Progress, this.onDownloadFileProgress); - ipcRenderer.on(Constants.IPC_Extract_Release_Complete, this.onExtractReleaseComplete); - ipcRenderer.on(Constants.IPC_Get_State_Reply, this.onGetStateReply); - ipcRenderer.on(Constants.IPC_Grab_Releases_Reply, this.onGrabReleasesReply); - ipcRenderer.on(Constants.IPC_Grab_UI_Releases_Reply, this.onGrabUiReleasesReply); - ipcRenderer.on(Constants.IPC_Install_Dependency_Reply, this.onInstallDependencyReply); - ipcRenderer.on(Constants.IPC_Install_Upgrade_Reply, this.onInstallUpgradeReply); - - ipcRenderer.send(Constants.IPC_Get_State, Constants.DATA_LOCATIONS[this.props.platform]); - Scheduler.scheduleJob('23 11 * * *', this.updateCheckScheduledJob); + for (const provider of Constants.PROVIDER_LIST) { + this.state[provider] = { + AutoMount: false, + AutoRestart: false, + MountLocation: '', + } } + + this.setRequestHandler(Constants.IPC_Check_Installed_Reply, this.onCheckInstalledReply); + this.setRequestHandler(Constants.IPC_Download_File_Complete, this.onDownloadFileComplete); + this.setRequestHandler(Constants.IPC_Download_File_Progress, this.onDownloadFileProgress); + this.setRequestHandler(Constants.IPC_Extract_Release_Complete, this.onExtractReleaseComplete); + this.setRequestHandler(Constants.IPC_Get_State_Reply, this.onGetStateReply); + this.setRequestHandler(Constants.IPC_Grab_Releases_Reply, this.onGrabReleasesReply); + this.setRequestHandler(Constants.IPC_Grab_UI_Releases_Reply, this.onGrabUiReleasesReply); + this.setRequestHandler(Constants.IPC_Install_Dependency_Reply, this.onInstallDependencyReply); + this.setRequestHandler(Constants.IPC_Install_Upgrade_Reply, this.onInstallUpgradeReply); + + this.sendRequest(Constants.IPC_Get_State, Constants.DATA_LOCATIONS[this.props.platform]); + Scheduler.scheduleJob('23 11 * * *', this.updateCheckScheduledJob); } state = { @@ -60,14 +62,9 @@ class App extends Component { DownloadingRelease: false, DownloadingUpgrade: false, ExtractActive: false, - Hyperspace: { - AutoMount: false, - MountLocation: '', - }, LocationsLookup: {}, MissingDependencies: [], MountsBusy: false, - Platform: 'unknown', Release: 3, ReleaseTypes: [ 'Release', @@ -76,10 +73,6 @@ class App extends Component { 'Alpha', ], InstalledVersion: 'none', - Sia: { - AutoMount: false, - MountLocation: '', - }, UpgradeAvailable: false, UpgradeData: {}, UpgradeDismissed: false, @@ -101,30 +94,24 @@ class App extends Component { } }; - checkVersionInstalled = (release, version, versionLookup) => { - if (!versionLookup) { - versionLookup = this.state.VersionLookup; - } - - const selectedVersion = versionLookup[this.state.ReleaseTypes[release]][version]; + checkVersionInstalled = () => { this.setState({ AllowDownload: false, - }); - - if (selectedVersion !== 'unavailable') { - if (ipcRenderer) { + }, ()=> { + const selectedVersion = this.getSelectedVersion(); + if (selectedVersion !== 'unavailable') { let dependencies = []; if (this.state.LocationsLookup[selectedVersion] && this.state.LocationsLookup[selectedVersion].dependencies) { dependencies = this.state.LocationsLookup[selectedVersion].dependencies; } - ipcRenderer.send(Constants.IPC_Check_Installed, { + this.sendRequest(Constants.IPC_Check_Installed, { Dependencies: dependencies, Directory: Constants.DATA_LOCATIONS[this.props.platform], Version: selectedVersion, }); } - } + }); }; closeErrorDisplay = () => { @@ -133,9 +120,7 @@ class App extends Component { } if (this.state.ErrorCritical) { - if (ipcRenderer) { - ipcRenderer.send(Constants.IPC_Shutdown); - } + this.sendRequest(Constants.IPC_Shutdown); } else { this.setState({ DisplayError: false, @@ -144,51 +129,63 @@ class App extends Component { } }; - componentWillUnmount = () => { - if (ipcRenderer) { - ipcRenderer.removeListener(Constants.IPC_Check_Installed_Reply, this.onCheckInstalledReply); - ipcRenderer.removeListener(Constants.IPC_Download_File_Complete, this.onDownloadFileComplete); - ipcRenderer.removeListener(Constants.IPC_Download_File_Progress, this.onDownloadFileProgress); - ipcRenderer.removeListener(Constants.IPC_Extract_Release_Complete, this.onExtractReleaseComplete); - ipcRenderer.removeListener(Constants.IPC_Get_State_Reply, this.onGetStateReply); - ipcRenderer.removeListener(Constants.IPC_Grab_Releases_Reply, this.onGrabReleasesReply); - ipcRenderer.removeListener(Constants.IPC_Grab_UI_Releases_Reply, this.onGrabUiReleasesReply); - ipcRenderer.removeListener(Constants.IPC_Install_Dependency_Reply, this.onInstallDependencyReply); - ipcRenderer.removeListener(Constants.IPC_Install_Upgrade_Reply, this.onInstallUpgradeReply); - } + extractFileNameFromURL = url => { + const parts = url.split('/'); + return parts[parts.length - 1]; + }; + + extractRelease = (data) => { + if (data.Success) { + const selectedVersion = this.getSelectedVersion(); + this.sendRequest(Constants.IPC_Extract_Release, { + Directory: Constants.DATA_LOCATIONS[this.props.platform], + Source: data.Destination, + Version: selectedVersion, + }); + } + + this.setState({ + DownloadActive: false, + DownloadProgress: 0.0, + DownloadingRelease: false, + ExtractActive: data.Success, + DownloadName: '', + }); + }; + + getSelectedVersion = () => { + return this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; }; grabReleases = () => { if (this.props.platform !== 'unknown') { - if (ipcRenderer) { - ipcRenderer.send(Constants.IPC_Grab_Releases); - ipcRenderer.send(Constants.IPC_Grab_UI_Releases); - } + this.sendRequest(Constants.IPC_Grab_Releases); + this.sendRequest(Constants.IPC_Grab_UI_Releases); } }; handleAutoMountChanged = (storageType, e) => { - let sia = { - ...this.state.Sia + const state = { + ...this.state[storageType], + AutoMount: e.target.checked, }; + this.setState({ + [storageType]: state, + }, ()=> { + this.saveState(); + }); + }; - let hyperspace = { - ...this.state.Hyperspace + handleAutoRestartChanged = (storageType, e) => { + const state = { + ...this.state[storageType], + AutoRestart: e.target.checked, }; - - if (storageType === 'Hyperspace') { - hyperspace.AutoMount = e.target.checked; - this.setState({ - Hyperspace: hyperspace, - }); - } else if (storageType === 'Sia') { - sia.AutoMount = e.target.checked; - this.setState({ - Sia: sia, - }); - } - - this.saveState(this.state.Release, this.state.Version, sia, hyperspace); + this.setState({ + [storageType]: state, + }, ()=> { + this.saveState(); + }); }; handleConfigClicked = (storageType) => { @@ -204,22 +201,17 @@ class App extends Component { }; handleDependencyDownload = (url) => { - if (ipcRenderer) { - const items = url.split('/'); - const fileName = items[items.length - 1]; - - this.setState({ - DownloadActive: true, - DownloadingDependency: true, - DownloadName: fileName, - }); - - ipcRenderer.send(Constants.IPC_Download_File, { + this.setState({ + DownloadActive: true, + DownloadingDependency: true, + DownloadName: this.extractFileNameFromURL(url), + }, ()=> { + this.sendRequest(Constants.IPC_Download_File, { Directory: Constants.DATA_LOCATIONS[this.props.platform], - Filename: fileName, + Filename: this.state.DownloadName, URL: url, }); - } + }); }; handleMountLocationChanged = (storageType, location) => { @@ -229,15 +221,9 @@ class App extends Component { }; this.setState({ [storageType]: state, + }, ()=> { + this.saveState(); }); - const hyperspace = (storageType === 'Hyperspace') ? state : { - ...this.state.Hyperspace, - }; - const sia = storageType === 'Sia' ? state : { - ...this.state.Sia, - }; - - this.saveState(this.state.Release, this.state.Version, sia, hyperspace); }; handleReleaseChanged = (e) => { @@ -246,54 +232,81 @@ class App extends Component { this.setState({ Release: val, Version: versionIndex + }, ()=> { + this.saveState(); + this.checkVersionInstalled( ); }); - this.saveState(val, versionIndex, this.state.Sia, this.state.Hyperspace); - this.checkVersionInstalled(val, versionIndex); }; handleReleaseDownload = () => { - const selectedVersion = this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; + const selectedVersion = this.getSelectedVersion(); const fileName = selectedVersion + '.zip'; - if (ipcRenderer) { - this.setState({ - DownloadActive: true, - DownloadingRelease: true, - DownloadName: fileName, - }); - - ipcRenderer.send(Constants.IPC_Download_File, { + this.setState({ + DownloadActive: true, + DownloadingRelease: true, + DownloadName: fileName, + }, () => { + this.sendRequest(Constants.IPC_Download_File, { Directory: Constants.DATA_LOCATIONS[this.props.platform], - Filename: fileName, + Filename: this.state.DownloadName, URL: this.state.LocationsLookup[selectedVersion].urls[0], }); - } + }); }; handleUIDownload = () => { - if (ipcRenderer) { - this.setState({ - DownloadActive: true, - DownloadingUpgrade: true, - DownloadName: 'UI Upgrade', - }); - - ipcRenderer.send(Constants.IPC_Download_File, { + this.setState({ + DownloadActive: true, + DownloadingUpgrade: true, + DownloadName: 'UI Upgrade', + }, ()=> { + const url = this.state.UpgradeData.urls[0]; + this.sendRequest(Constants.IPC_Download_File, { Directory: Constants.DATA_LOCATIONS[this.props.platform], - Filename: this.props.platform === 'win32' ? 'upgrade.exe' : 'upgrade', - URL: this.state.UpgradeData.urls[0], + Filename: this.props.platform === 'win32' ? 'upgrade.exe' : this.extractFileNameFromURL(url), + URL: url, }); - } else { - this.setState({UpgradeDismissed: true}); - } + }); }; handleVersionChanged = (e) => { - const val = parseInt(e.target.value, 10); this.setState({ - Version: val + Version: parseInt(e.target.value, 10), + }, ()=> { + this.saveState(); + this.checkVersionInstalled( ); }); - this.saveState(this.state.Release, val, this.state.Sia, this.state.Hyperspace); - this.checkVersionInstalled(this.state.Release, val); + }; + + installDependency = data => { + if (data.Success) { + this.sendRequest(Constants.IPC_Install_Dependency, { + Source: data.Destination, + URL: data.URL, + }); + } + + this.setState({ + DownloadActive: false, + DownloadProgress: 0.0, + DownloadingDependency: data.Success, + DownloadName: '', + }); + }; + + installUpgrade = data => { + if (data.Success) { + this.sendRequest(Constants.IPC_Install_Upgrade, { + Source: data.Destination, + }); + } else { + this.setState({ + DownloadActive: false, + DownloadProgress: 0.0, + DownloadingUpgrade: false, + DownloadName: '', + }); + } }; notifyAutoMountProcessed = () => { @@ -336,48 +349,11 @@ class App extends Component { onDownloadFileComplete = (event, arg) => { if (this.state.DownloadingRelease) { - if (arg.data.Success) { - const selectedVersion = this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; - ipcRenderer.send(Constants.IPC_Extract_Release, { - Directory: Constants.DATA_LOCATIONS[this.props.platform], - Source: arg.data.Destination, - Version: selectedVersion, - }); - } - - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingRelease: false, - ExtractActive: arg.data.Success, - DownloadName: '', - }); + this.extractRelease(arg.data); } else if (this.state.DownloadingDependency) { - if (arg.data.Success) { - ipcRenderer.send(Constants.IPC_Install_Dependency, { - Source: arg.data.Destination, - }); - } - - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingDependency: arg.data.Success, - DownloadName: '', - }); + this.installDependency(arg.data); } else if (this.state.DownloadingUpgrade) { - if (arg.data.Success) { - ipcRenderer.send(Constants.IPC_Install_Upgrade, { - Source: arg.data.Destination, - }); - } else { - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingUpgrade: false, - DownloadName: '', - }); - } + this.installUpgrade(arg.data); } else { this.setState({ DownloadActive: false, @@ -394,32 +370,41 @@ class App extends Component { }; onExtractReleaseComplete = (event, arg) => { - ipcRenderer.send(Constants.IPC_Delete_File, { + this.sendRequest(Constants.IPC_Delete_File, { FilePath: arg.data.Source, }); this.setState({ ExtractActive: false, + }, ()=> { + this.checkVersionInstalled( ); }); - this.checkVersionInstalled(this.state.Release, this.state.Version); }; onGetStateReply = (event, arg) => { if (arg.data) { - if (arg.data.Hyperspace.AutoMount === undefined) { - arg.data.Hyperspace['AutoMount'] = false; - } - if (arg.data.Sia.AutoMount === undefined) { - arg.data.Sia['AutoMount'] = false; - } - this.setState({ - Hyperspace: arg.data.Hyperspace, + let state = { Release: arg.data.Release, - Sia: arg.data.Sia, Version: arg.data.Version, + }; + + for (const provider of Constants.PROVIDER_LIST) { + let data = arg.data[provider] || this.state[provider]; + if (data.AutoMount === undefined) { + data['AutoMount'] = false; + } + if (data.AutoRestart === undefined) { + data['AutoRestart'] = false; + } + state[provider] = data; + } + + this.setState(state, ()=> { + this.grabReleases(); }); + } else { + this.grabReleases(); } - this.grabReleases(); }; onGrabReleasesReply = ()=> { @@ -428,7 +413,7 @@ class App extends Component { let version = this.state.Version; if ((version === -1) || !versionLookup[this.state.ReleaseTypes[this.state.Release]][version]) { version = latestVersion; - this.saveState(this.state.Release, version, this.state.Sia, this.state.Hyperspace); + this.saveState(version); } this.setState({ @@ -437,74 +422,80 @@ class App extends Component { Version: version, VersionAvailable: version !== latestVersion, VersionLookup: versionLookup, + }, () => { + this.checkVersionInstalled( ); }); - - this.checkVersionInstalled(this.state.Release, version, versionLookup); }; - axios.get(Constants.RELEASES_URL) - .then(response => { - const versionLookup = { - Alpha: response.data.Versions.Alpha[this.props.platform], - Beta: response.data.Versions.Beta[this.props.platform], - RC: response.data.Versions.RC[this.props.platform], - Release: response.data.Versions.Release[this.props.platform], - }; - const locationsLookup = { - ...response.data.Locations[this.props.platform], - }; + axios + .get(Constants.RELEASES_URL) + .then(response => { + const versionLookup = { + Alpha: response.data.Versions.Alpha[this.props.platform], + Beta: response.data.Versions.Beta[this.props.platform], + RC: response.data.Versions.RC[this.props.platform], + Release: response.data.Versions.Release[this.props.platform], + }; + const locationsLookup = { + ...response.data.Locations[this.props.platform], + }; - window.localStorage.setItem('releases', JSON.stringify({ - LocationsLookup: locationsLookup, - VersionLookup: versionLookup - })); - - doUpdate(locationsLookup, versionLookup); - }).catch(error => { - const releases = window.localStorage.getItem('releases'); - if (releases && (releases.length > 0)) { - const obj = JSON.parse(releases); - const locationsLookup = obj.LocationsLookup; - const versionLookup = obj.VersionLookup; + window.localStorage.setItem('releases', JSON.stringify({ + LocationsLookup: locationsLookup, + VersionLookup: versionLookup + })); doUpdate(locationsLookup, versionLookup); - } else { - this.setErrorState(error, null, true); - } - }); + }).catch(error => { + const releases = window.localStorage.getItem('releases'); + if (releases && (releases.length > 0)) { + const obj = JSON.parse(releases); + const locationsLookup = obj.LocationsLookup; + const versionLookup = obj.VersionLookup; + + doUpdate(locationsLookup, versionLookup); + } else { + this.setErrorState(error, null, true); + } + }); }; onGrabUiReleasesReply = ()=> { - axios.get(Constants.UI_RELEASES_URL) - .then(response => { - const data = response.data; - if (data.Versions && - data.Versions[this.props.platform] && - (data.Versions[this.props.platform].length > 0) && - (data.Versions[this.props.platform][0] !== this.props.version)) { + axios + .get(Constants.UI_RELEASES_URL) + .then(response => { + const data = response.data; + if (data.Versions && + data.Versions[this.props.platform] && + (data.Versions[this.props.platform].length > 0) && + (data.Versions[this.props.platform][0] !== this.props.version)) { + this.setState({ + UpgradeAvailable: true, + UpgradeDismissed: false, + UpgradeData: data.Locations[this.props.platform][data.Versions[this.props.platform][0]], + }); + } + }).catch(() => { this.setState({ - UpgradeAvailable: true, - UpgradeDismissed: false, - UpgradeData: data.Locations[this.props.platform][data.Versions[this.props.platform][0]], + UpgradeAvailable: false, + UpgradeData: {}, }); - } - }).catch(() => { - this.setState({ - UpgradeAvailable: false, - UpgradeData: {}, }); - }); }; onInstallDependencyReply = (event, arg) => { - ipcRenderer.send(Constants.IPC_Delete_File, { - FilePath: arg.data.Source, - }); - this.checkVersionInstalled(this.state.Release, this.state.Version); + if (arg.data.Success && arg.data.Source.toLowerCase().endsWith('.dmg')) { + this.waitForDependencyInstall(arg.data.URL); + } else { + this.sendRequest(Constants.IPC_Delete_File, { + FilePath: arg.data.Source, + }); + this.checkVersionInstalled(); + } }; onInstallUpgradeReply = (event, arg) => { - ipcRenderer.sendSync(Constants.IPC_Delete_File, { + this.sendSyncRequest(Constants.IPC_Delete_File, { FilePath: arg.data.Source, }); @@ -515,18 +506,19 @@ class App extends Component { }); }; - saveState = (release, version, sia, hyperspace)=> { - if (ipcRenderer) { - ipcRenderer.send(Constants.IPC_Save_State, { - Directory: Constants.DATA_LOCATIONS[this.props.platform], - State: { - Hyperspace: hyperspace, - Release: release, - Sia: sia, - Version: version, - } - }); + saveState = version => { + let state = { + Release: this.state.Release, + Version: version || this.state.Version, + }; + for (const provider of Constants.PROVIDER_LIST) { + state[provider] = this.state[provider]; } + + this.sendRequest(Constants.IPC_Save_State, { + Directory: Constants.DATA_LOCATIONS[this.props.platform], + State: state + }); }; setErrorState = (error, action, critical) => { @@ -544,10 +536,28 @@ class App extends Component { } }; + waitForDependencyInstall = (url) => { + const dep = this.state.MissingDependencies.find(d => { + return d.download === url; + }); + + const i = setInterval(()=> { + const ret = this.sendSyncRequest(Constants.IPC_Check_Dependency_Installed, { + File: dep.file, + }); + if (ret.data.Exists) { + clearInterval(i); + setTimeout(() => { + this.checkVersionInstalled(); + }, 10000); + } + }, 3000); + }; + render() { const selectedVersion = (this.state.Version === -1) ? 'unavailable' : - this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; + this.getSelectedVersion(); const downloadEnabled = this.state.AllowDownload && !this.state.MountsBusy && @@ -555,11 +565,18 @@ class App extends Component { (selectedVersion !== 'unavailable') && (selectedVersion !== this.state.InstalledVersion); - const allowMount = this.state.InstalledVersion !== 'none'; const missingDependencies = (this.state.MissingDependencies.length > 0); + const allowMount = this.state.InstalledVersion !== 'none' && !missingDependencies; + const allowConfig = this.state.LocationsLookup[selectedVersion] && this.state.LocationsLookup[selectedVersion].config_support; + const allowSiaPrime = this.state.LocationsLookup[selectedVersion] && + this.state.LocationsLookup[selectedVersion].siaprime_support; + + const noConsoleSupported = this.state.LocationsLookup[selectedVersion] && + this.state.LocationsLookup[selectedVersion].no_console_supported; + const showDependencies = missingDependencies && !this.state.DownloadActive; @@ -605,9 +622,8 @@ class App extends Component { - } - ) + ); } let downloadDisplay = null; @@ -616,7 +632,8 @@ class App extends Component { - ); + + ); } let upgradeDisplay = null; @@ -634,7 +651,7 @@ class App extends Component { let key = 0; mainContent.push((
+ style={{height: '25%'}}> - +
)); @@ -698,12 +721,12 @@ class App extends Component { col={dimensions => dimensions.columns - 6} colSpan={5} row={1} - rowSpan={remain=>remain - 2}/> + rowSpan={remain=>remain - 1}/>
- + {mainContent}
diff --git a/src/assets/images/release_available.png b/src/assets/images/release_available.png index 07e4493..4dbba4c 100644 Binary files a/src/assets/images/release_available.png and b/src/assets/images/release_available.png differ diff --git a/src/components/MountItem/MountItem.css b/src/components/MountItem/MountItem.css index e69de29..bd0afe4 100644 --- a/src/components/MountItem/MountItem.css +++ b/src/components/MountItem/MountItem.css @@ -0,0 +1,12 @@ +input.Input { + margin: 0; + padding: 3px; + border-radius: var(--border_radius); + background: rgba(160, 160, 160, 0.1); + border: none; + box-shadow: none; + outline: none; + color: var(--text_color); + box-sizing: border-box; + width: 100%; +} \ No newline at end of file diff --git a/src/components/MountItem/MountItem.js b/src/components/MountItem/MountItem.js index 1720088..5e8e0a3 100644 --- a/src/components/MountItem/MountItem.js +++ b/src/components/MountItem/MountItem.js @@ -18,7 +18,7 @@ export default CSSModules((props) => { rowSpan={6}> {e.preventDefault();} : props.configClicked} src={configureImage} style={{padding: 0, border: 0, margin: 0, cursor: 'pointer'}} width={'16px'}/> @@ -27,31 +27,47 @@ export default CSSModules((props) => { } let inputColumnSpan; - let inputControl = null; + let inputControls = null; if (props.platform === 'win32') { inputColumnSpan = 20; - inputControl = ; + inputControls = ; } else { - inputColumnSpan = 60; - inputControl = ( - - - ); + + )); + inputControls.push(( + + )); } - const buttonDisplay = props.allowMount ? + const buttonDisplay = props.allowMount || props.disabled ? (props.mounted ? 'Unmount' : 'Mount') : { width='19px'/>; const actionsDisplay = ( - ); const autoMountControl = ( - Auto-mount ); + const autoRestartControl = ( + + Restart + + ); + return ( {configButton} @@ -87,9 +116,10 @@ export default CSSModules((props) => { rowSpan={5} text={props.title} type={'Heading1'}/> - {inputControl} + {inputControls} {actionsDisplay} {autoMountControl} + {autoRestartControl} ); }, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js b/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js index f918152..ef1d15f 100644 --- a/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js +++ b/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js @@ -8,19 +8,57 @@ import Button from '../UI/Button/Button'; import UpgradeIcon from '../UpgradeIcon/UpgradeIcon'; export default CSSModules((props) => { - let optionsDisplay = null; + let optionsDisplay = []; + let key = 0; if (props.releaseExtracting) { - optionsDisplay = '}/> + optionsDisplay.push(( + (dimensions.columns / 3) * 2} + colSpan={'remain'} + key={key++} + rowSpan={4} + text={'Activating'} + textAlign={'left'} + type={'Heading2'}/> + )); + optionsDisplay.push(( + (dimensions.columns / 3) * 2} + colSpan={'remain'} + key={key++} + row={5} + rowSpan={7} + text={props.installedVersion} + textAlign={'left'}/> + )); + } else if (props.downloadDisabled) { + optionsDisplay.push(( + (dimensions.columns / 3) * 2} + colSpan={'remain'} + key={key++} + rowSpan={4} + text={'Installed'} + textAlign={'left'} + type={'Heading2'}/> + )); + + optionsDisplay.push(( + (dimensions.columns / 3) * 2} + colSpan={'remain'} + key={key++} + row={5} + rowSpan={7} + text={props.installedVersion} + textAlign={'left'}/> + )); } else { - optionsDisplay = ; + optionsDisplay.push(( + + )); } return ( @@ -56,18 +94,6 @@ export default CSSModules((props) => { row={5} rowSpan={7} selected={props.version}/> - (dimensions.columns / 3) * 2} - colSpan={'remain'} - rowSpan={4} - text={'Installed'} - textAlign={'left'} - type={'Heading2'}/> - (dimensions.columns / 3) * 2} - colSpan={'remain'} - row={5} - rowSpan={7} - text={props.installedVersion} - textAlign={'left'}/> {optionsDisplay} ); diff --git a/src/constants.js b/src/constants.js index 06b1a92..bd45c0d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,9 +9,28 @@ exports.DATA_LOCATIONS = { }; exports.UI_RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory-ui/raw/master/releases.json'; +exports.PROVIDER_LIST = [ + 'Hyperspace', + 'Sia', + 'SiaPrime' +]; + +exports.PROVIDER_ARG = { + hyperspace: '-hs', + sia: '', + siaprime: '-sp' +}; + +exports.IPC_Browse_Directory = 'browse_directory'; + +exports.IPC_Check_Dependency_Installed = 'check_dependency_installed'; +exports.IPC_Check_Dependency_Installed_Reply = 'check_dependency_installed'; + exports.IPC_Check_Installed = 'check_installed'; exports.IPC_Check_Installed_Reply = 'check_installed_reply'; +exports.IPC_Check_Mount_Location = 'check_mount_location'; + exports.IPC_Delete_File = 'delete_file'; exports.IPC_Detect_Mounts = 'detect_mounts'; diff --git a/src/containers/Configuration/Configuration.js b/src/containers/Configuration/Configuration.js index 5e3e393..ce89457 100644 --- a/src/containers/Configuration/Configuration.js +++ b/src/containers/Configuration/Configuration.js @@ -5,28 +5,22 @@ import Button from '../../components/UI/Button/Button'; import ConfigurationItem from '../../components/ConfigurationItem/ConfigurationItem'; import CSSModules from 'react-css-modules'; import Modal from '../../components/UI/Modal/Modal'; +import IPCContainer from '../IPCContainer/IPCContainer'; const Constants = require('../../constants'); -let ipcRenderer = null; -if (!process.versions.hasOwnProperty('electron')) { - ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); -} - -class Configuration extends Component { +class Configuration extends IPCContainer { constructor(props) { super(props); - if (ipcRenderer) { - ipcRenderer.on(Constants.IPC_Get_Config_Template_Reply, this.onGetConfigTemplateReply); - ipcRenderer.on(Constants.IPC_Get_Config_Reply, this.onGetConfigReply); - ipcRenderer.on(Constants.IPC_Set_Config_Values_Reply, this.onSetConfigValuesReply); + this.setRequestHandler(Constants.IPC_Get_Config_Template_Reply, this.onGetConfigTemplateReply); + this.setRequestHandler(Constants.IPC_Get_Config_Reply, this.onGetConfigReply); + this.setRequestHandler(Constants.IPC_Set_Config_Values_Reply, this.onSetConfigValuesReply); - ipcRenderer.send(Constants.IPC_Get_Config_Template, { - Directory: this.props.directory, - StorageType: this.props.storageType, - Version: this.props.version, - }); - } + this.sendRequest(Constants.IPC_Get_Config_Template, { + Directory: this.props.directory, + StorageType: this.props.storageType, + Version: this.props.version, + }); } state = { @@ -78,14 +72,6 @@ class Configuration extends Component { } }; - componentWillUnmount = () => { - if (ipcRenderer) { - ipcRenderer.removeListener(Constants.IPC_Get_Config_Reply, this.onGetConfigReply); - ipcRenderer.removeListener(Constants.IPC_Get_Config_Template_Reply, this.onGetConfigTemplateReply); - ipcRenderer.removeListener(Constants.IPC_Set_Config_Values_Reply, this.onSetConfigValuesReply); - } - }; - createItemList = (config, template) => { const objectList = []; const itemList = Object @@ -164,11 +150,12 @@ class Configuration extends Component { if (arg.data.Success) { this.setState({ Template: arg.data.Template, - }); - ipcRenderer.send(Constants.IPC_Get_Config, { - Directory: this.props.directory, - StorageType: this.props.storageType, - Version: this.props.version, + }, ()=> { + this.sendRequest(Constants.IPC_Get_Config, { + Directory: this.props.directory, + StorageType: this.props.storageType, + Version: this.props.version, + }); }); } else { this.props.errorHandler(arg.data.Error, () => { @@ -182,11 +169,9 @@ class Configuration extends Component { }; saveAndClose = () => { - if (ipcRenderer) { - this.setState({ - Saving: true, - }); - + this.setState({ + Saving: true, + }, ()=> { const changedItems = []; for (const item of this.state.ChangedItems) { changedItems.push({ @@ -206,13 +191,13 @@ class Configuration extends Component { } } - ipcRenderer.send(Constants.IPC_Set_Config_Values, { + this.sendRequest(Constants.IPC_Set_Config_Values, { Directory: this.props.directory, Items: changedItems, StorageType: this.props.storageType, Version: this.props.version, }); - } + }); }; render() { diff --git a/src/containers/IPCContainer/IPCContainer.js b/src/containers/IPCContainer/IPCContainer.js new file mode 100644 index 0000000..9fde958 --- /dev/null +++ b/src/containers/IPCContainer/IPCContainer.js @@ -0,0 +1,51 @@ +import {Component} from 'react'; + +export default class extends Component { + constructor(props) { + super(props); + + if (!process.versions.hasOwnProperty('electron')) { + this.ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); + } + } + + handlerList = {}; + ipcRenderer; + + componentWillUnmount() { + if (this.ipcRenderer) { + for (let name in this.handlerList) { + if (this.handlerList.hasOwnProperty(name)) { + this.ipcRenderer.removeListener(name, this.handlerList[name]); + } + } + } + + this.handlerList = {}; + }; + + sendRequest = (name, data) => { + if (this.ipcRenderer) { + this.ipcRenderer.send(name, data); + } + }; + + sendSyncRequest = (name, data) => { + if (this.ipcRenderer) { + return this.ipcRenderer.sendSync(name + '_sync', data); + } else { + return { + Success: false, + Error: 'IPC not available. Running in browser?', + }; + } + }; + + setRequestHandler = (name, callback) => { + if (this.ipcRenderer) { + this.handlerList[name] = callback; + this.ipcRenderer.on(name, callback); + } + }; + +}; \ No newline at end of file diff --git a/src/containers/MountItems/MountItems.js b/src/containers/MountItems/MountItems.js index c2c77ea..2da4b9e 100644 --- a/src/containers/MountItems/MountItems.js +++ b/src/containers/MountItems/MountItems.js @@ -1,140 +1,176 @@ import React from 'react'; -import {Component} from 'react'; +import Box from '../../components/UI/Box/Box'; +import Button from '../../components/UI/Button/Button'; import CSSModules from 'react-css-modules'; import styles from './MountItems.css'; +import Modal from '../../components/UI/Modal/Modal'; import MountItem from '../../components/MountItem/MountItem'; +import IPCContainer from '../IPCContainer/IPCContainer'; const Constants = require('../../constants'); -let ipcRenderer = null; -if (!process.versions.hasOwnProperty('electron')) { - ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); -} - -class MountItems extends Component { +class MountItems extends IPCContainer { constructor(props) { super(props); - if (ipcRenderer) { - ipcRenderer.on(Constants.IPC_Detect_Mounts_Reply, this.onDetectMountsReply); - ipcRenderer.on(Constants.IPC_Mount_Drive_Reply, this.onMountDriveReply); - ipcRenderer.on(Constants.IPC_Unmount_Drive_Reply, this.onUnmountDriveReply); - this.detectMounts(); + for (const provider of Constants.PROVIDER_LIST) { + this.state[provider] = { + AllowMount: false, + DriveLetters: [], + Mounted: false, + }; } + + this.setRequestHandler(Constants.IPC_Detect_Mounts_Reply, this.onDetectMountsReply); + this.setRequestHandler(Constants.IPC_Mount_Drive_Reply, this.onMountDriveReply); + this.setRequestHandler(Constants.IPC_Unmount_Drive_Reply, this.onUnmountDriveReply); + + this.detectMounts(); } + retryIntervals = {}; + state = { - Hyperspace: { - AllowMount: false, - DriveLetters: [], - Mounted: false, - PID: -1, - }, - Sia: { - AllowMount: false, - DriveLetters: [], - Mounted: false, - PID: -1, - }, + DisplayRetry: false, + RetryItems: {}, }; - componentWillUnmount = () => { - if (ipcRenderer) { - ipcRenderer.removeListener(Constants.IPC_Detect_Mounts_Reply, this.onDetectMountsReply); - ipcRenderer.removeListener(Constants.IPC_Mount_Drive_Reply, this.onMountDriveReply); - ipcRenderer.removeListener(Constants.IPC_Unmount_Drive_Reply, this.onUnmountDriveReply); + cancelRetryMount = (storageType, stateCallback) => { + clearInterval(this.retryIntervals[storageType]); + delete this.retryIntervals[storageType]; + + if (stateCallback) { + let retryItems = { + ...this.state.RetryItems, + }; + delete retryItems[storageType]; + this.setState({ + DisplayRetry: Object.keys(retryItems).length > 0, + RetryItems: retryItems, + }, stateCallback); } }; + componentWillUnmount() { + for (const storageType in this.state.RetryItems) { + if (this.state.RetryItems.hasOwnProperty(storageType)) { + this.cancelRetryMount(storageType); + } + } + + super.componentWillUnmount(); + }; + detectMounts = ()=> { - this.props.mountsBusy(true); - ipcRenderer.send(Constants.IPC_Detect_Mounts, { - Directory: this.props.directory, - Version: this.props.version, - }); - }; - - handleMountLocationChanged = (systemType, value) => { - if (this.props.platform === 'win32') { - this.props.changed(systemType, this.state[systemType].DriveLetters[value]); - } - else { - this.props.changed(systemType, value); - } - }; - - handleMountUnMount = (storageType, mount, location, pid) => { - if (ipcRenderer) { - const state = { - ...this.state[storageType], - AllowMount: false, - }; - this.setState({ - [storageType]: state, - }); - + if (!this.state.DisplayRetry) { this.props.mountsBusy(true); + this.sendRequest(Constants.IPC_Detect_Mounts, { + Directory: this.props.directory, + Version: this.props.version, + }); + } + }; - if (mount) { - ipcRenderer.send(Constants.IPC_Mount_Drive, { - Directory: this.props.directory, + handleBrowseLocation = (storageType, location) => { + location = this.sendSyncRequest(Constants.IPC_Browse_Directory, { + Title: storageType + ' Mount Location', + Location: location, + }); + if (location && (location.length > 0)) { + this.handleMountLocationChanged(storageType, location); + } + }; + + handleMountLocationChanged = (storageType, value) => { + if (this.props.platform === 'win32') { + this.props.changed(storageType, this.state[storageType].DriveLetters[value]); + } else { + this.props.changed(storageType, value); + } + }; + + handleMountUnMount = (storageType, mount, location) => { + if (!location || (location.trim().length === 0)) { + this.props.errorHandler('Mount location is not set'); + } else { + let allowAction = true; + if (mount && (this.props.platform !== 'win32')) { + const result = this.sendSyncRequest(Constants.IPC_Check_Mount_Location, { Location: location, - StorageType: storageType, - Version: this.props.version, }); - } else { - ipcRenderer.send(Constants.IPC_Unmount_Drive, { - Directory: this.props.directory, - Location: location, - PID: pid, - StorageType: storageType, - Version: this.props.version, + if (!result.Success) { + allowAction = false; + this.props.errorHandler(result.Error.toString()); + } + } + + if (allowAction) { + const storageState = { + ...this.state[storageType], + AllowMount: false, + }; + + this.props.mountsBusy(true); + + this.setState({ + [storageType]: storageState, + }, () => { + if (mount) { + this.sendRequest(Constants.IPC_Mount_Drive, { + Directory: this.props.directory, + Location: location, + NoConsoleSupported: this.props.noConsoleSupported, + StorageType: storageType, + Version: this.props.version, + }); + } else { + this.sendRequest(Constants.IPC_Unmount_Drive, { + Directory: this.props.directory, + Location: location, + StorageType: storageType, + Version: this.props.version, + }); + } }); } } }; onDetectMountsReply = (event, arg) => { - if (arg.data.Success) { - const sia = { - ...this.state.Sia, - AllowMount: true, - DriveLetters: (arg.data.DriveLetters.Sia), - Mounted: (arg.data.Locations.Sia.length > 0), - PID: arg.data.PIDS.Sia, - }; - const hs = { - ...this.state.Hyperspace, - AllowMount: true, - DriveLetters: (arg.data.DriveLetters.Hyperspace), - Mounted: (arg.data.Locations.Hyperspace.length > 0), - PID: arg.data.PIDS.Hyperspace, - }; + if (!this.state.DisplayRetry && arg.data.Success) { + let state = {}; + let mountsBusy = false; + for (const provider of Constants.PROVIDER_LIST) { + state[provider] = { + ...this.state[provider], + AllowMount: true, + DriveLetters: (arg.data.DriveLetters[provider]), + Mounted: (arg.data.Locations[provider].length > 0), + }; + mountsBusy = mountsBusy || state[provider].Mounted; + } + this.props.mountsBusy(mountsBusy); - this.setState({ - Hyperspace: hs, - Sia: sia, + this.setState(state, () => { + const updateMountLocation = (data, provider) => { + const providerLower = provider.toLowerCase(); + let location = data.Locations[provider]; + if (location.length === 0) { + location = (this.props.platform === 'win32') ? + this.props[providerLower].MountLocation || data.DriveLetters[provider][0] : + this.props[providerLower].MountLocation; + } + if (location !== this.props[providerLower].MountLocation) { + this.props.changed(provider, location); + } + }; + + for (const provider of Constants.PROVIDER_LIST) { + updateMountLocation(arg.data, provider); + } + + this.performAutoMount(); }); - - this.props.mountsBusy(hs.Mounted || sia.Mounted); - - let hsLocation = arg.data.Locations.Hyperspace; - if ((hsLocation.length === 0) && (this.props.platform === 'win32')) { - hsLocation = this.props.hyperspace.MountLocation || arg.data.DriveLetters.Hyperspace[0]; - } - if (hsLocation !== this.props.hyperspace.MountLocation) { - this.props.changed('Hyperspace', hsLocation); - } - - let siaLocation = arg.data.Locations.Sia; - if ((siaLocation.length === 0) && (this.props.platform === 'win32')) { - siaLocation = this.props.sia.MountLocation || arg.data.DriveLetters.Sia[0]; - } - if (siaLocation !== this.props.sia.MountLocation) { - this.props.changed('Sia', siaLocation); - } - - this.performAutoMount(); } else { this.props.errorHandler(arg.data.Error); } @@ -143,66 +179,134 @@ class MountItems extends Component { onMountDriveReply = (event, arg) => { const state = { ...this.state[arg.data.StorageType], - PID: arg.data.PID, Mounted: arg.data.Success, }; this.setState({ [arg.data.StorageType]: state, + }, ()=> { + this.detectMounts(); }); - - this.detectMounts(); }; onUnmountDriveReply = (event, arg) => { - this.detectMounts(); + if (arg && arg.data && !arg.data.Expected && arg.data.Location && this.props[arg.data.StorageType.toLowerCase()].AutoRestart) { + const storageType = arg.data.StorageType; + if (!this.state.RetryItems[storageType]) { + let retryItems = { + ...this.state.RetryItems + }; + retryItems[storageType] = { + RetrySeconds: 10, + }; + const storageState = { + ...this.state[arg.data.StorageType], + AllowMount: false, + Mounted: false, + }; + this.setState({ + [storageType]: storageState, + DisplayRetry: true, + RetryItems: retryItems, + }, () => { + this.retryIntervals[storageType] = setInterval(() => { + let retryItems = { + ...this.state.RetryItems, + }; + const retrySeconds = retryItems[storageType].RetrySeconds - 1; + if (retrySeconds === 0) { + this.cancelRetryMount(storageType, () => { + this.handleMountUnMount(storageType, true, arg.data.Location); + }); + } else { + retryItems[storageType].RetrySeconds = retrySeconds; + this.setState({ + RetryItems: retryItems, + }); + } + },1000); + }); + } + } else { + this.detectMounts(); + } }; performAutoMount = ()=> { if (this.props.processAutoMount) { this.props.autoMountProcessed(); - if (this.props.hyperspace.AutoMount && - !this.state.Hyperspace.Mounted && - (this.props.hyperspace.MountLocation.length > 0)) { - this.handleMountUnMount('Hyperspace', true, this.props.hyperspace.MountLocation); - } - if (this.props.sia.AutoMount && - !this.state.Sia.Mounted && - (this.props.sia.MountLocation.length > 0)) { - this.handleMountUnMount('Sia', true, this.props.sia.MountLocation); + const processAutoMount = (provider) => { + const providerLower = provider.toLowerCase(); + if (this.props[providerLower].AutoMount && + !this.state[provider].Mounted && + (this.props[providerLower].MountLocation.length > 0)) { + this.handleMountUnMount(provider, true, this.props[providerLower].MountLocation); + } + }; + + for (const provider of Constants.PROVIDER_LIST) { + processAutoMount(provider); } } }; render() { + let retryDisplay; + if (this.state.DisplayRetry) { + let retryList = []; + let retryListCount = 0; + for (const storageType in this.state.RetryItems) { + if (this.state.RetryItems.hasOwnProperty(storageType)) { + retryListCount++; + retryList.push(); + if (retryListCount < Object.keys(this.state.RetryItems).length) { + retryList.push(
); + } + } + } + + retryDisplay = ( + + +

Mount Failed

+ {retryList} +
+
+ ) + } + + let items = []; + for (const provider of Constants.PROVIDER_LIST) { + const providerLower = provider.toLowerCase(); + items.push(( + this.props.autoMountChanged(provider, e)} + autoRestart={this.props[providerLower].AutoRestart} + autoRestartChanged={(e)=>this.props.autoRestartChanged(provider, e)} + browseClicked={this.handleBrowseLocation} + changed={(e) => this.handleMountLocationChanged(provider, e.target.value)} + clicked={this.handleMountUnMount} + configClicked={()=>this.props.configClicked(provider)} + items={this.state[provider].DriveLetters} + key={'mi_' + items.length} + location={this.props[providerLower].MountLocation} + mounted={this.state[provider].Mounted} + platform={this.props.platform} + title={provider} /> + )); + if (items.length !== this.state.length) { + items.push(
) + } + } + return (
- this.props.autoMountChanged('Hyperspace', e)} - changed={(e) => this.handleMountLocationChanged('Hyperspace', e.target.value)} - clicked={this.handleMountUnMount} - configClicked={()=>this.props.configClicked('Hyperspace')} - items={this.state.Hyperspace.DriveLetters} - location={this.props.hyperspace.MountLocation} - mounted={this.state.Hyperspace.Mounted} - pid={this.state.Hyperspace.PID} - platform={this.props.platform} - title={'Hyperspace'}/> -
- this.props.autoMountChanged('Sia', e)} - changed={(e) => this.handleMountLocationChanged('Sia', e.target.value)} - clicked={this.handleMountUnMount} - configClicked={()=>this.props.configClicked('Sia')} - items={this.state.Sia.DriveLetters} - location={this.props.sia.MountLocation} - mounted={this.state.Sia.Mounted} - pid={this.state.Sia.PID} - platform={this.props.platform} - title={'Sia'}/> + {retryDisplay} + {items}
); } } diff --git a/src/index.css b/src/index.css index 9a4c0ce..8fd7ee9 100644 --- a/src/index.css +++ b/src/index.css @@ -14,11 +14,13 @@ --heading_text_color: rgba(161, 190, 219, 0.7); --heading_other_text_color: var(--heading_text_color); --text_color_transition: color 0.3s; + + --default_font_size: 4vmin } * { font-family: 'Nunito', sans-serif; - font-size: 5vh; + font-size: var(--default_font_size); } *::-moz-focus-inner { diff --git a/src/index.js b/src/index.js index a7d5c00..62b9876 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,11 @@ if (!process.versions.hasOwnProperty('electron')) { const ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); if (ipcRenderer) { ipcRenderer.on(Constants.IPC_Get_Platform_Reply, (event, arg) => { + if (arg.data === 'linux') { + let root = document.documentElement; + root.style.setProperty('--default_font_size', '4.8vmin'); + } + ReactDOM.render(, document.getElementById('root')); registerServiceWorker(); });