diff --git a/.gitignore b/.gitignore index 7106de7..f2b4e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ build/ chrome_data/ dist/ +/.cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fb5b637 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog # +## 1.0.1 ## +* Added configuration settings for Repertory 1.0.0-alpha.2 and above +* Fixed memory leak on component unmount +* Added error display +* Lighter tray icon on Windows +* Tray icon indicates mount status on Windows +* Various fixes/layout changes + +## 1.0.0 ## +* Initial release +* Windows support diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7aa51d6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +# Repertory UI MIT License # +### Copyright <2018> ### + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 257ba39..55811b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Repertory UI -![alt text](https://image.ibb.co/dpncxU/repertory_windows.png) - +![alt text](https://image.ibb.co/mnhA1z/repertory_1_0_1.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. # Downloads # diff --git a/electron.js b/electron.js index 9dce969..1cbcd7b 100644 --- a/electron.js +++ b/electron.js @@ -2,7 +2,7 @@ const {app, BrowserWindow, Tray, nativeImage, Menu} = require('electron'); const {ipcMain} = require('electron'); - +const Constants = require('./src/constants'); const path = require('path'); const url = require('url'); require('electron-debug')(); @@ -14,17 +14,19 @@ 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 mainWindow; let mainWindowTray; let mountedPIDs = []; function createWindow() { // Create the browser window. - const height = process.env.ELECTRON_START_URL ? 340 : 320; + const height = process.env.ELECTRON_START_URL ? 324 : 304; mainWindow = new BrowserWindow({ width: 425, height: height, resizable: false, + title: 'Repertory UI', webPreferences: { webSecurity: !process.env.ELECTRON_START_URL } @@ -46,14 +48,18 @@ function createWindow() { mainWindow = null }); - if (os.platform() === 'win32') { + 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 autoLauncher = new AutoLaunch({ name: 'Repertory UI', - path: path.resolve(path.join(app.getAppPath(), '..\\..\\repertory-ui.exe')), + path: appPath, }); - const image = nativeImage.createFromPath(path.join(__dirname, '/build/icon.ico')); - const contextMenu = Menu.buildFromTemplate([ + const image = nativeImage.createFromPath(path.join(__dirname, '/build/logo.png')); + mainContextWindow = Menu.buildFromTemplate([ { label: 'Visible', type: 'checkbox', click(item) { if (item.checked) { @@ -75,21 +81,30 @@ function createWindow() { autoLauncher.disable(); } } + }, + { + type: 'separator' + }, + { + label: 'Exit', click(item) { + app.quit(); + } } ]); - contextMenu.items[0].checked = true; - autoLauncher.isEnabled() - .then((enabled) => { - contextMenu.items[1].checked = enabled; + mainContextWindow.items[0].checked = true; + autoLauncher + .isEnabled() + .then((enabled) => { + mainContextWindow.items[1].checked = enabled; - mainWindowTray = new Tray(image); - mainWindowTray.setToolTip('Repertory UI'); - mainWindowTray.setContextMenu(contextMenu) - }) - .catch(() => { - app.quit(); - }); + mainWindowTray = new Tray(image); + mainWindowTray.setToolTip('Repertory UI'); + mainWindowTray.setContextMenu(mainContextWindow) + }) + .catch(() => { + app.quit(); + }); } } @@ -99,6 +114,10 @@ if (!instanceLock) { } else { app.on('second-instance', () => { if (mainWindow) { + mainWindow.show(); + if (mainContextWindow) { + mainContextWindow.items[0].checked = true; + } if (mainWindow.isMinimized()) { mainWindow.restore(); } @@ -125,7 +144,17 @@ if (!instanceLock) { }); } -ipcMain.on('check_installed', (event, data) => { +const standardIPCReply = (event, channel, data, error) => { + event.sender.send(channel, { + data: { + ...data, + Error: error, + Success: !error, + } + }); +}; + +ipcMain.on(Constants.IPC_Check_Installed, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); const destination = path.join(dataDirectory, data.Version); helpers @@ -136,27 +165,20 @@ ipcMain.on('check_installed', (event, data) => { exists = fs.existsSync(destination) && fs.lstatSync(destination).isDirectory(); } catch (e) { } - event.sender.send('check_installed_reply', { - data: { - Dependencies: dependencies, - Exists: exists, - Success: true, - Version: data.Version, - } - }); - }).catch((e) => { - event.sender.send('check_installed_reply', { - data: { - Dependencies: [], - Error: e, - Success: false, - Version: data.Version, - } + standardIPCReply(event, Constants.IPC_Check_Installed_Reply, { + Dependencies: dependencies, + Exists: exists, + Version: data.Version, }); + }).catch(error => { + standardIPCReply(event, Constants.IPC_Check_Installed_Reply, { + Dependencies: [], + Version: data.Version, + }, error); }); }); -ipcMain.on('delete_file', (event, data) => { +ipcMain.on(Constants.IPC_Delete_File, (event, data) => { try { if (fs.existsSync(data.FilePath)) { fs.unlinkSync(data.FilePath); @@ -165,7 +187,7 @@ ipcMain.on('delete_file', (event, data) => { } }); -ipcMain.on('detect_mounts', (event, data) => { +ipcMain.on(Constants.IPC_Detect_Mounts, (event, data) => { let driveLetters = { Hyperspace: [], Sia: [], @@ -202,6 +224,23 @@ ipcMain.on('detect_mounts', (event, data) => { } }; + 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')); + } + + mainWindowTray.setImage(image); + } + }; + const dataDirectory = helpers.resolvePath(data.Directory); helpers .detectRepertoryMounts(dataDirectory, data.Version) @@ -213,204 +252,225 @@ ipcMain.on('detect_mounts', (event, data) => { siaLocation = siaLocation.toUpperCase(); grabDriveLetters(hsLocation, siaLocation); } - event.sender.send('detect_mounts_reply', { - data: { - DriveLetters: driveLetters, - Locations: { - Hyperspace: hsLocation, - Sia: siaLocation, - }, - Success: true, - PIDS: { - Hyperspace: results.Hyperspace.PID, - Sia: results.Sia.PID, - } + 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, } }); }) - .catch((err) => { + .catch(error => { grabDriveLetters('', ''); - - event.sender.send('detect_mounts_reply', { - data: { - DriveLetters: driveLetters, - Error: err, - Success: false, - } - }); + setImage('', ''); + standardIPCReply(event, Constants.IPC_Detect_Mounts_Reply, { + DriveLetters: driveLetters, + }, error); }); }); -ipcMain.on('download_file', (event, data) => { +ipcMain.on(Constants.IPC_Download_File, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); const destination = path.join(dataDirectory, data.Filename); helpers.downloadFile(data.URL, destination, (progress) => { - event.sender.send('download_file_progress', { - data: { - Destination: destination, - Progress: progress, - URL: data.URL, - } - }); - }, (success, err) => { - event.sender.send('download_file_complete', { - data: { - Destination: destination, - Error: err, - Success: success, - URL: data.URL, - } + standardIPCReply(event, Constants.IPC_Download_File_Progress, { + Destination: destination, + Progress: progress, + URL: data.URL, }); + }, error => { + standardIPCReply(event, Constants.IPC_Download_File_Complete, { + Destination: destination, + URL: data.URL, + }, error); }); }); -ipcMain.on('extract_release', (event, data) => { +ipcMain.on(Constants.IPC_Extract_Release, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); const destination = path.join(dataDirectory, data.Version); helpers.mkDirByPathSync(destination); const stream = fs.createReadStream(data.Source); - stream.pipe(unzip.Extract({ path: destination })) - .on('error', (e) => { + stream + .pipe(unzip.Extract({ path: destination })) + .on('error', error => { try { helpers.removeDirectoryRecursively(destination); } catch (e) { } stream.close(); - event.sender.send('extract_release_complete', { - data: { - Error: e, - Source: data.Source, - Success: false, - } - }); + standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { + Source: data.Source, + }, error); }) .on('finish', () => { stream.close(); - event.sender.send('extract_release_complete', { - data: { - Source: data.Source, - Success: true, - } + standardIPCReply(event, Constants.IPC_Extract_Release_Complete, { + Source: data.Source, }); }); }); -ipcMain.on('get_platform', (event) => { - event.sender.send('get_platform_reply', { +ipcMain.on(Constants.IPC_Get_Config, (event, data) => { + const dataDirectory = helpers.resolvePath(data.Directory); + helpers + .getConfig(dataDirectory, data.Version, data.StorageType) + .then((data) => { + if (data.Code === 0) { + standardIPCReply(event, Constants.IPC_Get_Config_Reply, { + Config: data.Data, + }); + } else { + standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, data.Code); + } + }) + .catch(error => { + standardIPCReply(event, Constants.IPC_Get_Config_Reply, {}, error); + }); +}); + +ipcMain.on(Constants.IPC_Get_Config_Template, (event, data) => { + const dataDirectory = helpers.resolvePath(data.Directory); + helpers + .getConfigTemplate(dataDirectory, data.Version, data.StorageType) + .then((data) => { + standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, { + Template: data, + }); + }) + .catch(error => { + standardIPCReply(event, Constants.IPC_Get_Config_Template_Reply, {}, error); + }); +}); + +ipcMain.on(Constants.IPC_Get_Platform, (event) => { + event.sender.send(Constants.IPC_Get_Platform_Reply, { data: os.platform() }); }); -ipcMain.on('get_state', (event, data) => { +ipcMain.on(Constants.IPC_Get_State, (event, data) => { const dataDirectory = helpers.resolvePath(data); helpers.mkDirByPathSync(dataDirectory); const configFile = path.join(dataDirectory, 'settings.json'); if (fs.existsSync(configFile)) { - event.sender.send('get_state_reply', { + event.sender.send(Constants.IPC_Get_State_Reply, { data: JSON.parse(fs.readFileSync(configFile, 'utf8')), }); } else { - event.sender.send('get_state_reply', { + event.sender.send(Constants.IPC_Get_State_Reply, { data: null, }); } }); -ipcMain.on('grab_releases', (event) => { - event.sender.send('grab_releases_reply'); +ipcMain.on(Constants.IPC_Grab_Releases, (event) => { + standardIPCReply(event, Constants.IPC_Grab_Releases_Reply); }); -ipcMain.on('grab_ui_releases', (event) => { - event.sender.send('grab_ui_releases_reply'); +ipcMain.on(Constants.IPC_Grab_UI_Releases, (event) => { + standardIPCReply(event, Constants.IPC_Grab_UI_Releases_Reply); }); -ipcMain.on('install_dependency', (event, data) => { +ipcMain.on(Constants.IPC_Install_Dependency, (event, data) => { helpers .executeAndWait(data.Source) .then(()=> { - event.sender.send('install_dependency_reply', { - data: { - Source: data.Source, - Success: true, - } + standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { + Source: data.Source, }); }) - .catch((e)=> { - event.sender.send('install_dependency_reply', { - data: { - Error: e, - Source: data.Source, - Success: false, - } - }); + .catch(error => { + standardIPCReply(event, Constants.IPC_Install_Dependency_Reply, { + Source: data.Source, + }, error); }); }); -ipcMain.on('install_upgrade', (event, data) => { +ipcMain.on(Constants.IPC_Install_Upgrade, (event, data) => { helpers .executeAsync(data.Source) .then(()=> { mainWindow.close(); }) - .catch((e)=> { - event.sender.send('install_upgrade_reply', { - data: { - Error: e, - Source: data.Source, - Success: false, - } - }); + .catch(error => { + standardIPCReply(event, Constants.IPC_Install_Upgrade_Reply, { + Source: data.Source, + }, error); }); }); -ipcMain.on('mount_drive', (event, data) => { +ipcMain.on(Constants.IPC_Mount_Drive, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); - const errorHandler = (pid) => { + const errorHandler = (pid, error) => { mountedPIDs.splice(mountedPIDs.indexOf(pid), 1); - event.sender.send('unmount_drive_reply', { - data: { - PID: -1, - StorageType: data.StorageType, - Success: false, - } - }); + standardIPCReply(event, Constants.IPC_Unmount_Drive_Reply, { + PID: -1, + StorageType: data.StorageType, + }, error || Error(data.StorageType + ' Unmounted')); }; - helpers.executeMount(dataDirectory, data.Version, data.StorageType, data.Location, (_, pid)=> { - errorHandler(pid); + helpers.executeMount(dataDirectory, data.Version, data.StorageType, data.Location, (error, pid) => { + errorHandler(pid, error); }) - .then(pid=> { + .then(pid => { if (pid !== -1) { mountedPIDs.push(pid); } - event.sender.send('mount_drive_reply', { - data: { - PID: pid, - StorageType: data.StorageType, - Success: true, - } + standardIPCReply(event, Constants.IPC_Mount_Drive_Reply, { + PID: pid, + StorageType: data.StorageType, }); }) - .catch((_, pid) => { - errorHandler(pid); + .catch(error => { + errorHandler(-1, error); }); }); -ipcMain.on('save_state', (event, data) => { +ipcMain.on(Constants.IPC_Save_State, (event, data) => { const dataDirectory = helpers.resolvePath(data.Directory); helpers.mkDirByPathSync(dataDirectory); const configFile = path.join(dataDirectory, 'settings.json'); fs.writeFileSync(configFile, JSON.stringify(data.State), 'utf8'); }); -ipcMain.on('unmount_drive', (event, data) => { - helpers.stopProcessByPID(data.PID) - .then((pid)=> { - if (mountedPIDs.indexOf(pid) === -1) { - event.sender.send('unmount_drive_reply'); +ipcMain.on(Constants.IPC_Set_Config_Values, (event, data) => { + const dataDirectory = helpers.resolvePath(data.Directory); + const setConfigValue = (i) => { + if (i < data.Items.length) { + helpers + .setConfigValue(data.Items[i].Name, data.Items[i].Value, dataDirectory, data.StorageType, data.Version) + .then(() => { + setConfigValue(++i); + }) + .catch(() => { + setConfigValue(++i); + }); + } else { + standardIPCReply(event, Constants.IPC_Set_Config_Values_Reply, {}); } - }) - .catch((e) => { - console.log(e); - }); + }; + setConfigValue(0); +}); + +ipcMain.on(Constants.IPC_Shutdown, () => { + app.quit(); +}); + +ipcMain.on(Constants.IPC_Unmount_Drive, (event, data) => { + helpers + .stopProcessByPID(data.PID) + .then((pid)=> { + if (mountedPIDs.indexOf(pid) === -1) { + event.sender.send(Constants.IPC_Unmount_Drive_Reply); + } + }) + .catch((e) => { + console.log(e); + }); }); diff --git a/helpers.js b/helpers.js index 69ac777..c88b27e 100644 --- a/helpers.js +++ b/helpers.js @@ -80,18 +80,18 @@ module.exports.downloadFile = (url, destination, progressCallback, completeCallb response.data.on('end', () => { stream.end(() => { - completeCallback(true); + completeCallback(); }); }); response.data.on('error', (e) => { stream.end(() => { - completeCallback(false, e); + completeCallback(e); }); }); }) .catch((e)=> { - completeCallback(false, e); + completeCallback(e); }); }; @@ -195,6 +195,85 @@ module.exports.executeMount = (directory, version, storageType, location, exitCa }); }; +module.exports.getConfig = (directory, version, storageType) => { + return new Promise((resolve, reject) => { + const processOptions = { + detached: true, + shell: false, + windowsHide: true, + }; + + const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); + const args = []; + args.push('-dc'); + if (storageType.toLowerCase() === 'hyperspace') { + args.push('-hs'); + } + + const process = new spawn(command, args, processOptions); + let result = ''; + + process.on('error', (err) => { + reject(err); + }); + + process.stdout.on('data', (d)=> { + result += d; + }); + + process.on('exit', () => { + const lines = result + .replace(/\r\n/g, '\n') + .split('\n'); + + const code = parseInt(lines[0], 10); + if (code === 0) { + lines.shift(); + resolve({ + Code: code, + Data: JSON.parse(lines.join('\n')), + }); + } else { + resolve(code); + } + }); + process.unref(); + }); +}; + +module.exports.getConfigTemplate = (directory, version, storageType) => { + return new Promise((resolve, reject) => { + const processOptions = { + detached: true, + shell: false, + windowsHide: true, + }; + + const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); + const args = []; + args.push('-gt'); + if (storageType.toLowerCase() === 'hyperspace') { + args.push('-hs'); + } + + const process = new spawn(command, args, processOptions); + let result = ''; + + process.on('error', (err) => { + reject(err); + }); + + process.stdout.on('data', (d)=> { + result += d; + }); + + process.on('exit', () => { + resolve(JSON.parse(result)); + }); + process.unref(); + }); +}; + module.exports.getMissingDependencies = dependencies => { return new Promise((resolve, reject) => { if (!dependencies || (dependencies.length === 0)) { @@ -323,6 +402,37 @@ module.exports.resolvePath = str => { } }; +module.exports.setConfigValue = (name, value, directory, storageType, version) => { + return new Promise((resolve, reject) => { + const processOptions = { + detached: true, + shell: false, + windowsHide: true, + }; + + const command = path.join(directory, version, (os.platform() === 'win32') ? 'repertory.exe' : 'repertory'); + const args = []; + args.push('-set'); + args.push(name); + args.push(value); + if (storageType.toLowerCase() === 'hyperspace') { + args.push('-hs'); + } + + const process = new spawn(command, args, processOptions); + + process.on('error', (err) => { + reject(err); + }); + + process.on('exit', () => { + resolve(); + }); + + process.unref(); + }); +}; + module.exports.stopProcessByPID = pid => { return new Promise((resolve, reject) => { const processOptions = { diff --git a/package.json b/package.json index b6add2a..85f6bfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "repertory-ui", - "version": "1.0.0", + "version": "1.0.1", "private": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.1", @@ -47,6 +47,7 @@ "react-dev-utils": "^5.0.1", "react-dom": "^16.4.1", "react-loader-spinner": "^2.0.6", + "react-tooltip": "^3.8.4", "resolve": "1.6.0", "style-loader": "0.19.0", "sw-precache-webpack-plugin": "0.11.4", @@ -68,7 +69,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "electron": "3.0.0", + "electron": "^3.0.2", "electron-builder": "^20.28.4", "extract-text-webpack-plugin": "^3.0.2", "webpack-browser-plugin": "^1.0.20" @@ -121,6 +122,7 @@ "appId": "repertory-ui", "files": [ "./electron.js", + "./src/constants.js", "build/**/*", "node_modules/**/*", "./helpers.js" diff --git a/public/favicon.ico b/public/favicon.ico index 530403f..1c44c7e 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon_old.ico b/public/favicon_old.ico new file mode 100644 index 0000000..530403f Binary files /dev/null and b/public/favicon_old.ico differ diff --git a/public/icon.ico b/public/icon.ico index 391e4a8..1c44c7e 100644 Binary files a/public/icon.ico and b/public/icon.ico differ diff --git a/public/icon_old.ico b/public/icon_old.ico new file mode 100644 index 0000000..391e4a8 Binary files /dev/null and b/public/icon_old.ico differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..56d6c12 Binary files /dev/null and b/public/logo.png differ diff --git a/public/logo_both.png b/public/logo_both.png new file mode 100644 index 0000000..4010799 Binary files /dev/null and b/public/logo_both.png differ diff --git a/public/logo_hs.png b/public/logo_hs.png new file mode 100644 index 0000000..a7ce38c Binary files /dev/null and b/public/logo_hs.png differ diff --git a/public/logo_sia.png b/public/logo_sia.png new file mode 100644 index 0000000..bf5c63d Binary files /dev/null and b/public/logo_sia.png differ diff --git a/releases.json b/releases.json index f1be4a9..b7563ef 100644 --- a/releases.json +++ b/releases.json @@ -1,6 +1,12 @@ { "Locations": { "win32": { + "1.0.1": { + "hash": "", + "urls": [ + "https://sia.pixeldrain.com/api/file/Alo1IF1u/download" + ] + }, "1.0.0": { "hash": "", "urls": [ @@ -11,6 +17,7 @@ }, "Versions": { "win32": [ + "1.0.1", "1.0.0" ] } diff --git a/src/App.css b/src/App.css index 35d4011..93e8859 100644 --- a/src/App.css +++ b/src/App.css @@ -8,4 +8,23 @@ width: 100vw; background-image: url('./assets/images/background.jpg'); background-size: cover; +} + +.Container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.Header { + height: 28px; + margin-bottom: 8px; + box-sizing: border-box; +} + +.Content { + flex: 1; + box-sizing: border-box; } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 5f5582b..fe29cf7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,18 +1,22 @@ import React, {Component} from 'react'; -import CSSModules from 'react-css-modules'; +import axios from 'axios'; import styles from './App.css'; import Box from './components/UI/Box/Box'; -import DropDown from './components/UI/DropDown/DropDown'; -import * as Constants from './constants'; -import axios from 'axios'; -import MountItems from './containers/MountItems/MountItems'; +import Configuration from './containers/Configuration/Configuration'; +import CSSModules from 'react-css-modules'; import DependencyList from './components/DependencyList/DependencyList'; -import Button from './components/UI/Button/Button'; -import Modal from './components/UI/Modal/Modal'; import DownloadProgress from './components/DownloadProgress/DownloadProgress'; -import UpgradeUI from './components/UpgradeUI/UpgradeUI'; +import ErrorDetails from './components/ErrorDetails/ErrorDetails'; +import Grid from './components/UI/Grid/Grid'; +import Loading from './components/UI/Loading/Loading'; +import Modal from './components/UI/Modal/Modal'; +import MountItems from './containers/MountItems/MountItems'; +import ReleaseVersionDisplay from './components/ReleaseVersionDisplay/ReleaseVersionDisplay'; +import Text from './components/UI/Text/Text'; import UpgradeIcon from './components/UpgradeIcon/UpgradeIcon'; +import UpgradeUI from './components/UpgradeUI/UpgradeUI'; +const Constants = require('./constants'); const Scheduler = require('node-schedule'); let ipcRenderer = null; @@ -25,180 +29,30 @@ class App extends Component { super(props); if (ipcRenderer) { - ipcRenderer.on('get_platform_reply', (event, arg) => { - this.setState({ - Platform: arg.data, - }); - ipcRenderer.send('get_state', Constants.DATA_LOCATIONS[arg.data]); - }); + 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.on('get_state_reply', (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, - Release: arg.data.Release, - Sia: arg.data.Sia, - Version: arg.data.Version, - }); - } - this.grabReleases(); - }); - - ipcRenderer.on('grab_releases_reply', ()=> { - axios.get(Constants.RELEASES_URL) - .then(response => { - const versionLookup = { - Alpha: response.data.Versions.Alpha[this.state.Platform], - Beta: response.data.Versions.Beta[this.state.Platform], - RC: response.data.Versions.RC[this.state.Platform], - Release: response.data.Versions.Release[this.state.Platform], - }; - const locationsLookup = { - ...response.data.Locations[this.state.Platform], - }; - this.setState({ - AllowOptions: true, - LocationsLookup: locationsLookup, - VersionLookup: versionLookup, - }); - this.checkVersionInstalled(this.state.Release, this.state.Version, versionLookup); - }).catch(error => { - console.log(error); - }); - }); - - ipcRenderer.on('grab_ui_releases_reply', ()=> { - axios.get(Constants.UI_RELEASES_URL) - .then(response => { - const data = response.data; - if (data.Versions && - data.Versions[this.state.Platform] && - (data.Versions[this.state.Platform].length > 0) && - (data.Versions[this.state.Platform][0] !== this.props.version)) { - this.setState({ - UpgradeAvailable: true, - UpgradeDismissed: false, - UpgradeData: data.Locations[this.state.Platform][data.Versions[this.state.Platform][0]], - }); - } - }).catch(error => { - console.log(error); - }); - }); - - ipcRenderer.on('download_file_progress', (event, arg) => { - this.setState({ - DownloadProgress: arg.data.Progress, - }); - }); - - ipcRenderer.on('download_file_complete', (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('extract_release', { - Directory: Constants.DATA_LOCATIONS[this.state.Platform], - Source: arg.data.Destination, - Version: selectedVersion, - }); - } - - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingRelease: false, - ExtractActive: arg.data.Success, - DownloadName: '', - }); - } else if (this.state.DownloadingDependency) { - if (arg.data.Success) { - ipcRenderer.send('install_dependency', { - Source: arg.data.Destination, - }); - } - - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingDependency: arg.data.Success, - DownloadName: '', - }); - } else if (this.state.DownloadingUpgrade) { - if (arg.data.Success) { - ipcRenderer.send('install_upgrade', { - Source: arg.data.Destination, - }); - } else { - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadingUpgrade: false, - DownloadName: '', - }); - } - } else { - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadName: '', - }); - } - }); - - ipcRenderer.on('extract_release_complete', (event, arg) => { - ipcRenderer.send('delete_file', { - FilePath: arg.data.Source, - }); - - this.setState({ - ExtractActive: false, - }); - this.checkVersionInstalled(this.state.Release, this.state.Version); - }); - - ipcRenderer.on('check_installed_reply', (event, arg) => { - this.setState({ - AllowDownload: true, - DownloadingDependency: false, - MissingDependencies: arg.data.Dependencies, - RepertoryVersion: arg.data.Success && arg.data.Exists ? arg.data.Version : 'none', - }); - }); - - ipcRenderer.on('install_dependency_reply', (event, arg) => { - ipcRenderer.send('delete_file', { - FilePath: arg.data.Source, - }); - this.checkVersionInstalled(this.state.Release, this.state.Version); - }); - - ipcRenderer.on('install_upgrade_reply', (event, arg) => { - ipcRenderer.sendSync('delete_file', { - FilePath: arg.data.Source, - }); - - this.setState({ - DownloadActive: false, - DownloadProgress: 0.0, - DownloadName: '', - }); - }); - - ipcRenderer.send('get_platform'); + ipcRenderer.send(Constants.IPC_Get_State, Constants.DATA_LOCATIONS[this.props.platform]); Scheduler.scheduleJob('23 11 * * *', this.updateCheckScheduledJob); } } state = { - AllowOptions: false, AllowDownload: false, - AutoMountChecked: false, + AutoMountProcessed: false, + ConfigStorageType: null, + DisplayError: false, + DisplayMainContent: false, + Error: null, + ErrorAction: null, + ErrorCritical: false, DownloadActive: false, DownloadProgress: 0.0, DownloadingDependency: false, @@ -212,6 +66,7 @@ class App extends Component { }, LocationsLookup: {}, MissingDependencies: [], + MountsBusy: false, Platform: 'unknown', Release: 3, ReleaseTypes: [ @@ -220,7 +75,7 @@ class App extends Component { 'Beta', 'Alpha', ], - RepertoryVersion: 'none', + InstalledVersion: 'none', Sia: { AutoMount: false, MountLocation: '', @@ -228,7 +83,8 @@ class App extends Component { UpgradeAvailable: false, UpgradeData: {}, UpgradeDismissed: false, - Version: 0, + Version: -1, + VersionAvailable: false, VersionLookup: { Alpha: [ 'unavailable' @@ -255,25 +111,58 @@ class App extends Component { AllowDownload: false, }); - if (ipcRenderer) { - let dependencies = []; - if (this.state.LocationsLookup[selectedVersion] && this.state.LocationsLookup[selectedVersion].dependencies) { - dependencies = this.state.LocationsLookup[selectedVersion].dependencies; - } + if (selectedVersion !== 'unavailable') { + if (ipcRenderer) { + let dependencies = []; + if (this.state.LocationsLookup[selectedVersion] && this.state.LocationsLookup[selectedVersion].dependencies) { + dependencies = this.state.LocationsLookup[selectedVersion].dependencies; + } - ipcRenderer.send('check_installed', { - Dependencies: dependencies, - Directory: Constants.DATA_LOCATIONS[this.state.Platform], - Version: selectedVersion, + ipcRenderer.send(Constants.IPC_Check_Installed, { + Dependencies: dependencies, + Directory: Constants.DATA_LOCATIONS[this.props.platform], + Version: selectedVersion, + }); + } + } + }; + + closeErrorDisplay = () => { + if (this.state.ErrorAction) { + this.state.ErrorAction(); + } + + if (this.state.ErrorCritical) { + if (ipcRenderer) { + ipcRenderer.send(Constants.IPC_Shutdown); + } + } else { + this.setState({ + DisplayError: false, + Error: null, }); } }; + 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); + } + }; + grabReleases = () => { - if (this.state.Platform !== 'unknown') { + if (this.props.platform !== 'unknown') { if (ipcRenderer) { - ipcRenderer.send('grab_releases'); - ipcRenderer.send('grab_ui_releases'); + ipcRenderer.send(Constants.IPC_Grab_Releases); + ipcRenderer.send(Constants.IPC_Grab_UI_Releases); } } }; @@ -302,6 +191,18 @@ class App extends Component { this.saveState(this.state.Release, this.state.Version, sia, hyperspace); }; + handleConfigClicked = (storageType) => { + this.setState({ + ConfigStorageType: storageType, + }) + }; + + handleConfigClosed = () => { + this.setState({ + ConfigStorageType: null, + }); + }; + handleDependencyDownload = (url) => { if (ipcRenderer) { const items = url.split('/'); @@ -313,8 +214,8 @@ class App extends Component { DownloadName: fileName, }); - ipcRenderer.send('download_file', { - Directory: Constants.DATA_LOCATIONS[this.state.Platform], + ipcRenderer.send(Constants.IPC_Download_File, { + Directory: Constants.DATA_LOCATIONS[this.props.platform], Filename: fileName, URL: url, }); @@ -341,12 +242,13 @@ class App extends Component { handleReleaseChanged = (e) => { const val = parseInt(e.target.value, 10); + const versionIndex = this.state.VersionLookup[this.state.ReleaseTypes[val]].length - 1; this.setState({ Release: val, - Version: 0 + Version: versionIndex }); - this.saveState(val, 0, this.state.Sia, this.state.Hyperspace); - this.checkVersionInstalled(val, 0); + this.saveState(val, versionIndex, this.state.Sia, this.state.Hyperspace); + this.checkVersionInstalled(val, versionIndex); }; handleReleaseDownload = () => { @@ -359,8 +261,8 @@ class App extends Component { DownloadName: fileName, }); - ipcRenderer.send('download_file', { - Directory: Constants.DATA_LOCATIONS[this.state.Platform], + ipcRenderer.send(Constants.IPC_Download_File, { + Directory: Constants.DATA_LOCATIONS[this.props.platform], Filename: fileName, URL: this.state.LocationsLookup[selectedVersion].urls[0], }); @@ -375,9 +277,9 @@ class App extends Component { DownloadName: 'UI Upgrade', }); - ipcRenderer.send('download_file', { - Directory: Constants.DATA_LOCATIONS[this.state.Platform], - Filename: this.state.Platform === 'win32' ? 'upgrade.exe' : 'upgrade', + ipcRenderer.send(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], }); } else { @@ -395,13 +297,228 @@ class App extends Component { }; notifyAutoMountProcessed = () => { - this.setState({AutoMountChecked: true}); + this.setState({AutoMountProcessed: true}); + }; + + notifyMountsBusy = (busy) => { + this.setState({MountsBusy: busy}) + }; + + onCheckInstalledReply = (event, arg) => { + const action = () => { + const installedVersion = arg.data.Success && arg.data.Exists ? arg.data.Version : 'none'; + let versionAvailable = false; + + if (installedVersion !== 'none') { + const latestVersion = this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]].length - 1; + let version = this.state.Version; + if (version === -1) { + version = latestVersion; + } + versionAvailable = version !== latestVersion; + } + + this.setState({ + AllowDownload: true, + DownloadingDependency: false, + MissingDependencies: arg.data.Dependencies, + InstalledVersion: installedVersion, + VersionAvailable: versionAvailable, + }); + }; + + if (arg.data.Success) { + action(); + } else { + this.setErrorState(arg.data.Error, action); + } + }; + + 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: '', + }); + } 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: '', + }); + } 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: '', + }); + } + } else { + this.setState({ + DownloadActive: false, + DownloadProgress: 0.0, + DownloadName: '', + }); + } + }; + + onDownloadFileProgress = (event, arg) => { + this.setState({ + DownloadProgress: arg.data.Progress, + }); + }; + + onExtractReleaseComplete = (event, arg) => { + ipcRenderer.send(Constants.IPC_Delete_File, { + FilePath: arg.data.Source, + }); + + this.setState({ + ExtractActive: false, + }); + 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, + Release: arg.data.Release, + Sia: arg.data.Sia, + Version: arg.data.Version, + }); + } + this.grabReleases(); + }; + + onGrabReleasesReply = ()=> { + const doUpdate = (locationsLookup, versionLookup) => { + const latestVersion = versionLookup[this.state.ReleaseTypes[this.state.Release]].length - 1; + 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.setState({ + DisplayMainContent: true, + LocationsLookup: locationsLookup, + Version: version, + VersionAvailable: version !== latestVersion, + VersionLookup: versionLookup, + }); + + 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], + }; + + 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; + + 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)) { + this.setState({ + UpgradeAvailable: true, + UpgradeDismissed: false, + UpgradeData: data.Locations[this.props.platform][data.Versions[this.props.platform][0]], + }); + } + }).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); + }; + + onInstallUpgradeReply = (event, arg) => { + ipcRenderer.sendSync(Constants.IPC_Delete_File, { + FilePath: arg.data.Source, + }); + + this.setState({ + DownloadActive: false, + DownloadProgress: 0.0, + DownloadName: '', + }); }; saveState = (release, version, sia, hyperspace)=> { if (ipcRenderer) { - ipcRenderer.send('save_state', { - Directory: Constants.DATA_LOCATIONS[this.state.Platform], + ipcRenderer.send(Constants.IPC_Save_State, { + Directory: Constants.DATA_LOCATIONS[this.props.platform], State: { Hyperspace: hyperspace, Release: release, @@ -412,38 +529,77 @@ class App extends Component { } }; + setErrorState = (error, action, critical) => { + this.setState({ + DisplayError: true, + Error: error, + ErrorAction: action, + ErrorCritical: critical, + }); + }; + updateCheckScheduledJob = () => { - if (this.state.Platform !== 'unknown') { - if (ipcRenderer) { - ipcRenderer.send('grab_ui_releases'); - } + if (this.props.platform !== 'unknown') { + this.grabReleases(); } }; render() { - const selectedVersion = this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; - const downloadEnabled = this.state.AllowDownload && - !this.state.DownloadActive && - (((selectedVersion !== 'unavailable') && (selectedVersion !== this.state.RepertoryVersion))); - const allowMount = this.state.RepertoryVersion !== 'none'; - const missingDependencies = (this.state.MissingDependencies.length > 0); + const selectedVersion = (this.state.Version === -1) ? + 'unavailable' : + this.state.VersionLookup[this.state.ReleaseTypes[this.state.Release]][this.state.Version]; - let mountDisplay = null; - if (allowMount) { - mountDisplay = ; + const downloadEnabled = this.state.AllowDownload && + !this.state.MountsBusy && + !this.state.DownloadActive && + (selectedVersion !== 'unavailable') && + (selectedVersion !== this.state.InstalledVersion); + + const allowMount = this.state.InstalledVersion !== 'none'; + const missingDependencies = (this.state.MissingDependencies.length > 0); + const allowConfig = this.state.LocationsLookup[selectedVersion] && + this.state.LocationsLookup[selectedVersion].config_support; + + const showDependencies = missingDependencies && + !this.state.DownloadActive; + + const showConfig = !missingDependencies && + this.state.ConfigStorageType && + allowConfig; + + const showUpgrade = this.state.UpgradeAvailable && + !this.state.DisplayError && + !showConfig && + !missingDependencies && + !this.state.DownloadActive && + !this.state.UpgradeDismissed; + + let errorDisplay = null; + if (this.state.DisplayError) { + errorDisplay = ( + + + + ); + } + + let configDisplay = null; + if (showConfig) { + configDisplay = ( + + + + ); } let dependencyDisplay = null; - if (missingDependencies && !this.state.DownloadActive) { + if (showDependencies) { dependencyDisplay = ( - + ); } - let releaseDisplay = null; - if (this.state.ExtractActive) { - releaseDisplay =

{'Activating <' + selectedVersion + '>'}

- } else { - releaseDisplay = ; - } - let upgradeDisplay = null; - if (!missingDependencies && - !this.state.DownloadActive && - this.state.UpgradeAvailable && - !this.state.UpgradeDismissed) { + if (showUpgrade) { upgradeDisplay = ( - this.setState({UpgradeDismissed: true})}/> + this.setState({UpgradeDismissed: true})} + upgrade={this.handleUIDownload}/> ); } - let options = null; - if (this.state.AllowOptions) { - options = ( - - - - - - - - - - - - - - - - - - - -
-

Release

-
-

Version

-
-

Installed

-
- - - - - {this.state.RepertoryVersion} -
- {releaseDisplay} -
- {mountDisplay} -
); + let mainContent = []; + if (this.state.DisplayMainContent) { + let key = 0; + mainContent.push(( +
+ +
+ )); + + if (allowMount) { + mainContent.push(( +
+ +
+ )); + } + } else { + mainContent = } return (
+ {errorDisplay} {dependencyDisplay} {upgradeDisplay} {downloadDisplay} - - - - - - - - -
- -

{'Repertory UI v' + this.props.version}

-
- this.setState({UpgradeDismissed: false})}/> -
-
- - {options} - + {configDisplay} +
+
+ + + + this.setState({UpgradeDismissed: false})} + col={dimensions => dimensions.columns - 6} + colSpan={5} + row={1} + rowSpan={remain=>remain - 2}/> + + +
+
+ + {mainContent} + +
+
); } diff --git a/src/assets/images/configure.png b/src/assets/images/configure.png new file mode 100644 index 0000000..e6ed2e0 Binary files /dev/null and b/src/assets/images/configure.png differ diff --git a/src/assets/images/release_available.png b/src/assets/images/release_available.png new file mode 100644 index 0000000..07e4493 Binary files /dev/null and b/src/assets/images/release_available.png differ diff --git a/src/assets/images/upgrade_available.png b/src/assets/images/upgrade_available.png deleted file mode 100644 index dbd19d0..0000000 Binary files a/src/assets/images/upgrade_available.png and /dev/null differ diff --git a/src/components/ConfigurationItem/ConfigurationItem.css b/src/components/ConfigurationItem/ConfigurationItem.css new file mode 100644 index 0000000..c880836 --- /dev/null +++ b/src/components/ConfigurationItem/ConfigurationItem.css @@ -0,0 +1,36 @@ +.ConfigurationItem { + margin: 0; + padding: 0; +} + +input.Input { + display: block; + margin: 0; + padding: 2px; + 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; +} + +.Select { + display: block; + margin: 0; + padding: 2px; + 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; +} + +.Option { + background: rgba(10, 10, 15, 0.8); + border-color: rgba(10, 10, 20, 0.9); + color: var(--text_color); +} \ No newline at end of file diff --git a/src/components/ConfigurationItem/ConfigurationItem.js b/src/components/ConfigurationItem/ConfigurationItem.js new file mode 100644 index 0000000..0f76ce2 --- /dev/null +++ b/src/components/ConfigurationItem/ConfigurationItem.js @@ -0,0 +1,107 @@ +import React from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './ConfigurationItem.css'; + +export default CSSModules((props) => { + const handleChanged = (e) => { + const target = e.target; + if (target.type === 'checkbox') { + target.value = e.target.checked ? "true" : "false"; + } + props.changed(target); + }; + + let data; + switch (props.template.type) { + case "bool": + data = handleChanged(e)} + type={'checkbox'}/>; + break; + + case "double": + data = handleChanged(e)} + step={"0.01"} + styleName='Input' + type={'number'} + value={parseFloat(props.value).toFixed(2)}/>; + break; + + case "list": + const options = props.items.map((s, i) => { + return ( + + ); + }); + + data = ( + + ); + break; + + case "string": + data = handleChanged(e)} + styleName='Input' + type={'text'} + value={props.value}/>; + break; + + case "uint8": + data = handleChanged(e)} + styleName='Input' + type={'number'} + value={props.value}/>; + break; + + case "uint16": + data = handleChanged(e)} + styleName='Input' + type={'number'} + value={props.value}/>; + break; + + case "uint32": + data = handleChanged(e)} + styleName='Input' + type={'number'} + value={props.value}/>; + break; + + case "uint64": + data = handleChanged(e)} + styleName='Input' + type={'number'} + value={props.value}/>; + break; + + default: + data =
{props.value}
; + } + + return ( +
+ + + + + + + +
{props.label}{data}
+
+ ); +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/DependencyList/DependencyList.js b/src/components/DependencyList/DependencyList.js index 4a5b155..1d387a4 100644 --- a/src/components/DependencyList/DependencyList.js +++ b/src/components/DependencyList/DependencyList.js @@ -8,15 +8,15 @@ export default CSSModules((props) => { const items = props.dependencies.map((k, i)=> { return ( ); }); return ( - +

Missing Dependencies

diff --git a/src/components/DownloadProgress/DownloadProgress.js b/src/components/DownloadProgress/DownloadProgress.js index 7263e8e..60adaf9 100644 --- a/src/components/DownloadProgress/DownloadProgress.js +++ b/src/components/DownloadProgress/DownloadProgress.js @@ -6,7 +6,7 @@ import styles from './DownloadProgress.css'; export default CSSModules((props) => { return ( - +

{'Downloading ' + props.display}

diff --git a/src/components/ErrorDetails/ErrorDetails.css b/src/components/ErrorDetails/ErrorDetails.css new file mode 100644 index 0000000..62616a8 --- /dev/null +++ b/src/components/ErrorDetails/ErrorDetails.css @@ -0,0 +1,11 @@ +.Heading { + color: var(--text_color_error); + text-align: center; + margin-bottom: 4px; +} + +.Content { + max-height: 60vh; + overflow-y: auto; + margin-bottom: 8px; +} \ No newline at end of file diff --git a/src/components/ErrorDetails/ErrorDetails.js b/src/components/ErrorDetails/ErrorDetails.js new file mode 100644 index 0000000..6bff639 --- /dev/null +++ b/src/components/ErrorDetails/ErrorDetails.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Box from '../UI/Box/Box'; +import Button from '../UI/Button/Button'; +import CSSModules from 'react-css-modules'; +import styles from './ErrorDetails.css'; + +export default CSSModules((props) => { + return ( + +

Application Error

+
+

{props.error.toString()}

+
+ +
+ ); + +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/MountItem/MountItem.css b/src/components/MountItem/MountItem.css index d6d8234..e69de29 100644 --- a/src/components/MountItem/MountItem.css +++ b/src/components/MountItem/MountItem.css @@ -1,4 +0,0 @@ -.MountItem { - width: 100%; -} - diff --git a/src/components/MountItem/MountItem.js b/src/components/MountItem/MountItem.js index eb0c45c..1720088 100644 --- a/src/components/MountItem/MountItem.js +++ b/src/components/MountItem/MountItem.js @@ -4,49 +4,92 @@ import styles from './MountItem.css'; import DropDown from '../UI/DropDown/DropDown'; import Button from '../UI/Button/Button'; import Loader from 'react-loader-spinner'; +import Text from '../UI/Text/Text'; +import Grid from '../UI/Grid/Grid'; +import configureImage from '../../assets/images/configure.png'; +import RootElem from '../../hoc/RootElem/RootElem'; export default CSSModules((props) => { - let inputControl = null; - let mountWidth = '70%'; - if (props.platform === 'win32') { - inputControl = ; - mountWidth = '18%'; - } else { - inputControl = ; + let configButton = null; + let secondRow = 6; + if (props.allowConfig) { + configButton = ( + + + + ); } - let actionDisplay = null; - if (props.allowMount) { - actionDisplay = ; + let inputColumnSpan; + let inputControl = null; + if (props.platform === 'win32') { + inputColumnSpan = 20; + inputControl = ; + } else { - actionDisplay = ; + inputColumnSpan = 60; + inputControl = ( + + + ); } + + const buttonDisplay = props.allowMount ? + (props.mounted ? 'Unmount' : 'Mount') : + ; + + const actionsDisplay = ( + ); + + const autoMountControl = ( + + Auto-mount + + ); + return ( -
-

{props.title}

- - - - - - - - -
{inputControl} - {actionDisplay} - - Auto-mount -
-
+ + {configButton} + + {inputControl} + {actionsDisplay} + {autoMountControl} + ); }, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.css b/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js b/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js new file mode 100644 index 0000000..f918152 --- /dev/null +++ b/src/components/ReleaseVersionDisplay/ReleaseVersionDisplay.js @@ -0,0 +1,74 @@ +import React from 'react'; +import styles from './ReleaseVersionDisplay.css'; +import CSSModules from 'react-css-modules'; +import DropDown from '../UI/DropDown/DropDown'; +import Grid from '../UI/Grid/Grid'; +import Text from '../UI/Text/Text'; +import Button from '../UI/Button/Button'; +import UpgradeIcon from '../UpgradeIcon/UpgradeIcon'; + +export default CSSModules((props) => { + let optionsDisplay = null; + if (props.releaseExtracting) { + optionsDisplay = '}/> + } else { + optionsDisplay = ; + } + + return ( + + columns / 3} + rowSpan={4} + text={'Release'} + textAlign={'left'} + type={'Heading2'}/> + remain / 3 - 1} + disabled={props.disabled} + items={props.releaseTypes} + row={5} + rowSpan={7} + selected={props.release}/> + dimensions.columns / 3} + colSpan={remain=>remain / 2} + rowSpan={4} + text={'Version'} + textAlign={'left'} + type={'Heading2'}/> + ((dimensions.columns / 3) * 2) - 6} + colSpan={4} + release + rowSpan={4}/> + dimensions.columns / 3} + colSpan={remain=>remain / 2 - 1} + disabled={props.disabled} + items={props.versions} + 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} + + ); +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UI/Box/Box.js b/src/components/UI/Box/Box.js index 480b511..42e44ac 100644 --- a/src/components/UI/Box/Box.js +++ b/src/components/UI/Box/Box.js @@ -3,7 +3,8 @@ import CSSModules from 'react-css-modules'; import styles from './Box.css'; export default CSSModules((props) => { - const styleList = ['Box']; + const styleList = []; + styleList.push('Box'); if (props.dxDark) { styleList.push('Darker'); } diff --git a/src/components/UI/Button/Button.css b/src/components/UI/Button/Button.css index c1164b1..9aa5877 100644 --- a/src/components/UI/Button/Button.css +++ b/src/components/UI/Button/Button.css @@ -1,7 +1,6 @@ .Button { display: block; text-align: center; - margin: 0; padding: 4px; outline: 0; color: var(--text_color); @@ -10,7 +9,10 @@ border: none; text-decoration: none; text-outline: none; - width: 70px; + vertical-align: center; + height: 100%; + width: 100%; + overflow: hidden; } .Button:hover:enabled { @@ -20,7 +22,7 @@ } .Button:hover:disabled { - cursor: default; + cursor: no-drop; } .Button:active, diff --git a/src/components/UI/DropDown/DropDown.css b/src/components/UI/DropDown/DropDown.css index b05cc4b..fcc23c7 100644 --- a/src/components/UI/DropDown/DropDown.css +++ b/src/components/UI/DropDown/DropDown.css @@ -1,5 +1,7 @@ .DropDown { + display: block; width: 100%; + height: 100%; margin: 0; padding: 0; } @@ -21,4 +23,17 @@ background: rgba(10, 10, 15, 0.8); border-color: rgba(10, 10, 20, 0.9); color: var(--text_color); +} + +.Select:hover:enabled { + cursor: pointer; +} + +.Select:hover:disabled { + cursor: no-drop; +} + +.Select:active, +.Select.active { + cursor: pointer; } \ No newline at end of file diff --git a/src/components/UI/DropDown/DropDown.js b/src/components/UI/DropDown/DropDown.js index 5ec0ec4..cc16361 100644 --- a/src/components/UI/DropDown/DropDown.js +++ b/src/components/UI/DropDown/DropDown.js @@ -1,6 +1,6 @@ import React from 'react'; -import CSSModules from 'react-css-modules'; import styles from './DropDown.css'; +import CSSModules from 'react-css-modules'; export default CSSModules((props) => { const options = props.items.map((s, i) => { diff --git a/src/components/UI/Grid/Grid.css b/src/components/UI/Grid/Grid.css new file mode 100644 index 0000000..185b7af --- /dev/null +++ b/src/components/UI/Grid/Grid.css @@ -0,0 +1,18 @@ +.Grid { + margin: 0; + padding: 0; + display: grid; + box-sizing: border-box; + height: 100%; + width: 100%; + overflow-y: auto; + overflow-x: auto; +} + +.GridOwner { + padding: 0; + margin: 0; + width: 100%; + height: 100%; + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/components/UI/Grid/Grid.js b/src/components/UI/Grid/Grid.js new file mode 100644 index 0000000..ad92a61 --- /dev/null +++ b/src/components/UI/Grid/Grid.js @@ -0,0 +1,126 @@ +import React, {Component} from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './Grid.css'; +import GridComponent from './GridComponent/GridComponent'; + +export default CSSModules(class extends Component { + constructor(props) { + super(props); + window.addEventListener("resize", this.updateSizeAsync); + } + + state = { + calculated: false, + dimensions: { + columns: 0, + rows: 0 + } + }; + + calculateDimensions = (size) => { + return { + columns: Math.floor(size.width / 4), + rows: Math.floor(size.height / 4) + }; + }; + + getSize = () => { + const elem = this.refs.GridOwner; + return { + height: elem ? elem.clientHeight : 0, + width: elem ? elem.clientWidth : 0 + }; + }; + + updateSize = () => { + const state = { + ...this.state + }; + const size = this.getSize(); + const dimensions = this.calculateDimensions(size); + if (state.dimensions !== dimensions) { + this.setState({ + calculated: true, + dimensions: dimensions + }) + } + }; + + updateSizeAsync = () => { + return new Promise((done) => { + this.updateSize(); + done(); + }); + }; + + componentDidMount = () => { + this.updateSizeAsync(); + }; + + componentWillUnmount = () => { + window.removeEventListener("resize", this.updateSizeAsync); + }; + + render() { + let children = null; + const dimensions = this.state.dimensions; + if (this.state.calculated) { + children = React.Children.map(this.props.children, (child, i) => { + if (child) { + let row = child.props.row || 0; + if (typeof(row) === 'function') { + row = row(dimensions); + } + + let col = child.props.col || 0; + if (typeof(col) === 'function') { + col = col(dimensions); + } + + let rowSpan = child.props.rowSpan; + if (typeof(rowSpan) === 'function') { + rowSpan = rowSpan(dimensions.rows - row, dimensions.rows); + } + + let colSpan = child.props.colSpan; + if (typeof(colSpan) === 'function') { + colSpan = colSpan(dimensions.columns - col, dimensions.columns); + } + + rowSpan = rowSpan ? (rowSpan === 'remain' ? (dimensions.rows - row) : rowSpan) : null; + colSpan = colSpan ? (colSpan === 'remain' ? dimensions.columns - col : colSpan) : null; + + return {child}; + } else { + return null; + } + }) + .filter(i => i !== null); + } + + const style = { + style: { + gridTemplateColumns: '4px '.repeat(dimensions.columns).trim(), + gridTemplateRows: '4px '.repeat(dimensions.rows).trim(), + gridAutoColumns: '4px', + gridAutoRows: '4px' + } + }; + + return ( +
+
+ {children} +
+
+ ) + }; + +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UI/Grid/GridComponent/GridComponent.css b/src/components/UI/Grid/GridComponent/GridComponent.css new file mode 100644 index 0000000..ca842fa --- /dev/null +++ b/src/components/UI/Grid/GridComponent/GridComponent.css @@ -0,0 +1,7 @@ +.GridComponent { + padding: 0; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/src/components/UI/Grid/GridComponent/GridComponent.js b/src/components/UI/Grid/GridComponent/GridComponent.js new file mode 100644 index 0000000..686fa3e --- /dev/null +++ b/src/components/UI/Grid/GridComponent/GridComponent.js @@ -0,0 +1,21 @@ +import React from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './GridComponent.css'; + +export default CSSModules((props) => { + const style = { + style: { + gridRowStart: Math.floor(props.row + 1), + gridRowEnd: 'span ' + Math.floor(props.rowSpan || 1), + gridColumnStart: Math.floor(props.col + 1), + gridColumnEnd: 'span ' + Math.floor(props.colSpan || 1) + } + }; + + return ( +
+ {props.children} +
+ ); + +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UI/Loading/Loading.css b/src/components/UI/Loading/Loading.css new file mode 100644 index 0000000..4784845 --- /dev/null +++ b/src/components/UI/Loading/Loading.css @@ -0,0 +1,18 @@ +.Loading { + margin: 0; + padding: 0; + height: inherit; + width: 100%; + box-sizing: border-box; +} + +.Content { + margin: 0; + padding: 0; + position: relative; + top: 50%; left: 50%; + transform: translate(-50%,-50%); + width: 28px; + height: 28px; + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/components/UI/Loading/Loading.js b/src/components/UI/Loading/Loading.js new file mode 100644 index 0000000..ef08431 --- /dev/null +++ b/src/components/UI/Loading/Loading.js @@ -0,0 +1,17 @@ +import React from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './Loading.css' +import Loader from 'react-loader-spinner'; + +export default CSSModules((props) => { + return ( +
+
+ +
+
); +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UI/Modal/Modal.css b/src/components/UI/Modal/Modal.css index c5090ac..19e5f32 100644 --- a/src/components/UI/Modal/Modal.css +++ b/src/components/UI/Modal/Modal.css @@ -17,3 +17,11 @@ transform: translate(-50%, -50%); z-index: 2001; } + +.Modal.Critical { + z-index: 2100; +} + +.Content.Critical { + z-index: 2101; +} diff --git a/src/components/UI/Modal/Modal.js b/src/components/UI/Modal/Modal.js index 057f05b..6dc7223 100644 --- a/src/components/UI/Modal/Modal.js +++ b/src/components/UI/Modal/Modal.js @@ -3,11 +3,20 @@ import CSSModules from 'react-css-modules'; import styles from './Modal.css' export default CSSModules((props) => { + let modalStyles = []; + let contentStyles = []; + modalStyles.push('Modal'); + contentStyles.push('Content'); + if (props.critical) { + modalStyles.push('Critical'); + contentStyles.push('Critical'); + } + return (
-
+
{props.children}
); diff --git a/src/components/UI/Text/Text.css b/src/components/UI/Text/Text.css new file mode 100644 index 0000000..b03a3ae --- /dev/null +++ b/src/components/UI/Text/Text.css @@ -0,0 +1,39 @@ +.TextOwner { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; +} + +.Text { + display: inline-block; + padding: 0; + margin: 0; + text-decoration: none; + text-align: center; + vertical-align: center; + color: var(--text_color); +} + +.Heading1 { + font-weight: bold; + color: var(--heading_text_color); +} + +.Heading2 { + font-weight: bold; + color: var(--heading_other_text_color); +} + +.Heading3 { + font-weight: bold; + color: var(--heading_other_text_color); +} + +.AltTextColor { + color: var(--heading_other_text_color); +} \ No newline at end of file diff --git a/src/components/UI/Text/Text.js b/src/components/UI/Text/Text.js new file mode 100644 index 0000000..6cc5a05 --- /dev/null +++ b/src/components/UI/Text/Text.js @@ -0,0 +1,28 @@ +import React from 'react'; +import CSSModules from 'react-css-modules'; +import styles from './Text.css'; + +export default CSSModules((props) => { + const styleList = []; + styleList.push('Text'); + if (props.type) { + styleList.push(props.type); + } + + let style = {...props.style}; + if (props.textAlign) { + style['textAlign'] = props.textAlign.toLowerCase(); + } + + const text = ( +
{props.text} +
); + + return props.noOwner ? text : ( +
+ {text} +
); + +}, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UpgradeIcon/UpgradeIcon.css b/src/components/UpgradeIcon/UpgradeIcon.css index cf9e26c..8afe7ce 100644 --- a/src/components/UpgradeIcon/UpgradeIcon.css +++ b/src/components/UpgradeIcon/UpgradeIcon.css @@ -1,14 +1,21 @@ -.UpgradeIcon { - display: block; - margin: 0; +.Owner { padding: 0; - width: 20px; - height: 20px; + margin: 0; + width: 100%; + height: 100%; border: 0; box-sizing: border-box; cursor: pointer; } -div.UpgradeIcon { - cursor: default; -} \ No newline at end of file +.UpgradeIcon { + display: block; + margin: 0; + padding: 0; + object-fit: contain; + border: 0; + max-width: 100%; + max-height: 100%; + box-sizing: border-box; + opacity: 0.65; + } \ No newline at end of file diff --git a/src/components/UpgradeIcon/UpgradeIcon.js b/src/components/UpgradeIcon/UpgradeIcon.js index 11a7fcb..7165531 100644 --- a/src/components/UpgradeIcon/UpgradeIcon.js +++ b/src/components/UpgradeIcon/UpgradeIcon.js @@ -1,10 +1,29 @@ import React from 'react'; import CSSModules from 'react-css-modules'; import styles from './UpgradeIcon.css'; -import availableImage from '../../assets/images/upgrade_available.png'; +import availableImage from '../../assets/images/release_available.png'; +import ReactTooltip from 'react-tooltip'; export default CSSModules((props) => { - return props.available ? - : -
; + let placement = 'left'; + let toolTipText = 'UI Upgrade Available'; + if (props.release) { + placement='bottom'; + toolTipText = 'New Release Available'; + } + + return props + .available ? + ( +
+

+ +

+ {toolTipText} +
+ ) + : null; }, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/components/UpgradeUI/UpgradeUI.js b/src/components/UpgradeUI/UpgradeUI.js index 7e795d6..3702c65 100644 --- a/src/components/UpgradeUI/UpgradeUI.js +++ b/src/components/UpgradeUI/UpgradeUI.js @@ -6,22 +6,22 @@ import styles from './UpgradeUI.css'; export default CSSModules((props) => { return ( - +

UI Upgrade Available

- - - - + + + +
- - - -
+ + + +
); diff --git a/src/constants.js b/src/constants.js index 741afc9..06b1a92 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,62 @@ -export const RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory/raw/master/releases.json'; -export const DATA_LOCATIONS = { +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory/raw/master/releases.json'; +exports.DATA_LOCATIONS = { linux: '~/.local/repertory/ui', darwin: '~/Library/Application Support/repertory/ui', - win32: '%LOCALAPPDATA%\\repertory\\ui', + win32: '%LOCALAPPDATA%\\repertory\\ui' }; -export const UI_RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory-ui/raw/master/releases.json'; \ No newline at end of file +exports.UI_RELEASES_URL = 'https://bitbucket.org/blockstorage/repertory-ui/raw/master/releases.json'; + +exports.IPC_Check_Installed = 'check_installed'; +exports.IPC_Check_Installed_Reply = 'check_installed_reply'; + +exports.IPC_Delete_File = 'delete_file'; + +exports.IPC_Detect_Mounts = 'detect_mounts'; +exports.IPC_Detect_Mounts_Reply = 'detect_mounts_reply'; + +exports.IPC_Download_File = 'download_file'; +exports.IPC_Download_File_Complete = 'download_file_complete'; +exports.IPC_Download_File_Progress = 'download_file_progress'; + +exports.IPC_Extract_Release = 'extract_release'; +exports.IPC_Extract_Release_Complete = 'extract_release_complete'; + +exports.IPC_Get_Config = 'get_config'; +exports.IPC_Get_Config_Reply = 'get_config_reply'; + +exports.IPC_Get_Config_Template = 'get_config_template'; +exports.IPC_Get_Config_Template_Reply = 'get_config_template_reply'; + +exports.IPC_Get_Platform = 'get_platform'; +exports.IPC_Get_Platform_Reply = 'get_platform_reply'; + +exports.IPC_Get_State = 'get_state'; +exports.IPC_Get_State_Reply = 'get_state_reply'; + +exports.IPC_Grab_Releases = 'grab_releases'; +exports.IPC_Grab_Releases_Reply = 'grab_releases_reply'; + +exports.IPC_Grab_UI_Releases = 'grab_ui_releases'; +exports.IPC_Grab_UI_Releases_Reply = 'grab_ui_releases_reply'; + +exports.IPC_Install_Dependency = 'install_dependency'; +exports.IPC_Install_Dependency_Reply = 'install_dependency_reply'; + +exports.IPC_Install_Upgrade = 'install_upgrade'; +exports.IPC_Install_Upgrade_Reply = 'install_upgrade_reply'; + +exports.IPC_Mount_Drive = 'mount_drive'; +exports.IPC_Mount_Drive_Reply = 'mount_drive_reply'; + +exports.IPC_Save_State = 'save_state'; + +exports.IPC_Set_Config_Values = 'set_config_values'; +exports.IPC_Set_Config_Values_Reply = 'set_config_values_reply'; + +exports.IPC_Shutdown = 'shutdown'; + +exports.IPC_Unmount_Drive = 'unmount_drive'; +exports.IPC_Unmount_Drive_Reply = 'unmount_drive_reply'; \ No newline at end of file diff --git a/src/containers/Configuration/Configuration.css b/src/containers/Configuration/Configuration.css new file mode 100644 index 0000000..675f8c8 --- /dev/null +++ b/src/containers/Configuration/Configuration.css @@ -0,0 +1,6 @@ +.Configuration { + width: 90vw; + height: 90vh; + padding: 4px; + margin: 0; +} \ No newline at end of file diff --git a/src/containers/Configuration/Configuration.js b/src/containers/Configuration/Configuration.js new file mode 100644 index 0000000..5e3e393 --- /dev/null +++ b/src/containers/Configuration/Configuration.js @@ -0,0 +1,295 @@ +import React, {Component} from 'react'; +import styles from './Configuration.css'; +import Box from '../../components/UI/Box/Box'; +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'; + +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 { + 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); + + ipcRenderer.send(Constants.IPC_Get_Config_Template, { + Directory: this.props.directory, + StorageType: this.props.storageType, + Version: this.props.version, + }); + } + } + + state = { + ChangedItems: [], + ChangedObjectLookup: null, + ObjectLookup: {}, + OriginalItemList: [], + OriginalObjectLookup: {}, + ItemList: [], + Saving: false, + ShowAdvanced: false, + Template: {} + }; + + checkSaveRequired = () => { + const changedItems = []; + let i = 0; + for (const item of this.state.ItemList) { + if (this.state.OriginalItemList[i++].value !== item.value) { + changedItems.push(item); + } + } + + let changedObjectLookup = null; + for (const key of Object.keys(this.state.ObjectLookup)) { + const changedObjectItems = []; + let j = 0; + for (const item of this.state.ObjectLookup[key]) { + if (this.state.OriginalObjectLookup[key][j++].value !== item.value) { + changedObjectItems.push(item); + } + } + + if (changedObjectItems.length > 0) { + if (changedObjectLookup === null) { + changedObjectLookup = {}; + } + changedObjectLookup[key] = changedObjectItems; + } + } + + if ((changedItems.length > 0) || changedObjectLookup) { + this.setState({ + ChangedItems: changedItems, + ChangedObjectLookup: changedObjectLookup, + }); + } else { + this.props.closed(); + } + }; + + 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 + .keys(config) + .map(key => { + return { + advanced: template[key] ? template[key].advanced : false, + label: key, + value: config[key], + }; + }) + .filter(i=> { + let ret = template[i.label]; + if (ret && (template[i.label].type === 'object')) { + objectList.push(i); + ret = false; + } + return ret; + }); + + return { + ObjectList: objectList, + ItemList: itemList, + } + }; + + handleItemChanged = (target, idx) => { + const itemList = [ + ...this.state.ItemList + ]; + itemList[idx].value = target.value.toString(); + this.setState({ + ItemList: itemList + }); + }; + + handleObjectItemChanged = (target, name, idx) => { + const itemList = [ + ...this.state.ObjectLookup[name] + ]; + const objectLookup = { + ...this.state.ObjectLookup, + }; + + itemList[idx].value = target.value.toString(); + objectLookup[name] = itemList; + this.setState({ + ObjectLookup: objectLookup, + }); + }; + + onGetConfigReply = (event, arg) => { + if (arg.data.Success) { + const list = this.createItemList(arg.data.Config, this.state.Template); + const itemListCopy = JSON.parse(JSON.stringify(list.ItemList)); + + let objectLookup = {}; + for (const obj of list.ObjectList) { + const list2 = this.createItemList(obj.value, this.state.Template[obj.label].template); + objectLookup[obj.label] = list2.ItemList; + } + const objectLookupCopy = JSON.parse(JSON.stringify(objectLookup)); + + this.setState({ + ItemList: list.ItemList, + ObjectLookup: objectLookup, + OriginalItemList: itemListCopy, + OriginalObjectLookup: objectLookupCopy, + }); + } else { + this.props.errorHandler(arg.data.Error); + } + }; + + onGetConfigTemplateReply = (event, arg) => { + 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, + }); + } else { + this.props.errorHandler(arg.data.Error, () => { + this.props.closed(); + }); + } + }; + + onSetConfigValuesReply = () => { + this.props.closed(); + }; + + saveAndClose = () => { + if (ipcRenderer) { + this.setState({ + Saving: true, + }); + + const changedItems = []; + for (const item of this.state.ChangedItems) { + changedItems.push({ + Name: item.label, + Value: item.value, + }); + } + + if (this.state.ChangedObjectLookup) { + for (const key of Object.keys(this.state.ChangedObjectLookup)) { + for (const item of this.state.ChangedObjectLookup[key]) { + changedItems.push({ + Name: key + '.' + item.label, + Value: item.value, + }); + } + } + } + + ipcRenderer.send(Constants.IPC_Set_Config_Values, { + Directory: this.props.directory, + Items: changedItems, + StorageType: this.props.storageType, + Version: this.props.version, + }); + } + }; + + render() { + let confirmSave = null; + if ((this.state.ChangedItems.length > 0) || this.state.ChangedObjectLookup) { + confirmSave = ( + + +

Save Changes?

+ + + + + +
+
+
+ ); + } + + const configurationItems = this.state.ItemList + .map((k, i) => { + return ( + ((this.state.ShowAdvanced && k.advanced) || !k.advanced) ? + this.handleItemChanged(e, i)} + items={this.state.Template[k.label].items} + key={i} + label={k.label} + template={this.state.Template[k.label]} + value={k.value}/> : + null) + }); + + let objectItems = []; + for (const key of Object.keys(this.state.ObjectLookup)) { + objectItems.push(( +
+

{key}

+
+ { + this.state.ObjectLookup[key].map((k, i) => { + return ( + ((this.state.ShowAdvanced && k.advanced) || !k.advanced) ? + this.handleObjectItemChanged(e, key, i)} + items={this.state.Template[key].template[k.label].items} + key={i} + label={k.label} + template={this.state.Template[key].template[k.label]} + value={k.value}/> : + null) + }) + } +
+
+ )); + } + + return ( +
+ {confirmSave} + +
+ X +
+

{this.props.storageType + ' Configuration'}

+
+ {objectItems} + {(configurationItems.length > 0) ?

Settings

: null} + {configurationItems} +
+
+
+ ); + } +} + +export default CSSModules(Configuration, styles, {allowMultiple: true}); \ No newline at end of file diff --git a/src/containers/MountItems/MountItems.css b/src/containers/MountItems/MountItems.css index f6e19ab..d9fbcb3 100644 --- a/src/containers/MountItems/MountItems.css +++ b/src/containers/MountItems/MountItems.css @@ -1,4 +1,6 @@ .MountItems { - margin-top: 10px; + padding: 0; + margin: 0; width: 100%; + box-sizing: border-box; } \ No newline at end of file diff --git a/src/containers/MountItems/MountItems.js b/src/containers/MountItems/MountItems.js index d37e624..c2c77ea 100644 --- a/src/containers/MountItems/MountItems.js +++ b/src/containers/MountItems/MountItems.js @@ -4,6 +4,8 @@ import CSSModules from 'react-css-modules'; import styles from './MountItems.css'; import MountItem from '../../components/MountItem/MountItem'; +const Constants = require('../../constants'); + let ipcRenderer = null; if (!process.versions.hasOwnProperty('electron')) { ipcRenderer = ((window && window.require) ? window.require('electron').ipcRenderer : null); @@ -13,64 +15,9 @@ class MountItems extends Component { constructor(props) { super(props); if (ipcRenderer) { - ipcRenderer.on('detect_mounts_reply', (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, - }; - - this.setState({ - Hyperspace: hs, - Sia: sia, - }); - - 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(); - } - }); - - ipcRenderer.on('mount_drive_reply', (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(); - }); - - ipcRenderer.on('unmount_drive_reply', (event, arg) => { - this.detectMounts(); - }); + 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(); } @@ -91,8 +38,17 @@ class MountItems extends Component { }, }; + 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); + } + }; + detectMounts = ()=> { - ipcRenderer.send('detect_mounts', { + this.props.mountsBusy(true); + ipcRenderer.send(Constants.IPC_Detect_Mounts, { Directory: this.props.directory, Version: this.props.version, }); @@ -117,15 +73,17 @@ class MountItems extends Component { [storageType]: state, }); + this.props.mountsBusy(true); + if (mount) { - ipcRenderer.send('mount_drive', { + ipcRenderer.send(Constants.IPC_Mount_Drive, { Directory: this.props.directory, Location: location, StorageType: storageType, Version: this.props.version, }); } else { - ipcRenderer.send('unmount_drive', { + ipcRenderer.send(Constants.IPC_Unmount_Drive, { Directory: this.props.directory, Location: location, PID: pid, @@ -136,6 +94,69 @@ class MountItems extends Component { } }; + 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, + }; + + this.setState({ + Hyperspace: hs, + Sia: sia, + }); + + 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); + } + }; + + 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(); + }; + + onUnmountDriveReply = (event, arg) => { + this.detectMounts(); + }; + performAutoMount = ()=> { if (this.props.processAutoMount) { this.props.autoMountProcessed(); @@ -155,28 +176,33 @@ class MountItems extends Component { render() { return (
- this.props.autoMountChanged('Hyperspace', e)} - mounted={this.state.Hyperspace.Mounted} - items={this.state.Hyperspace.DriveLetters} - platform={this.props.platform} - title={'Hyperspace'} - location={this.props.hyperspace.MountLocation} changed={(e) => this.handleMountLocationChanged('Hyperspace', e.target.value)} clicked={this.handleMountUnMount} - pid={this.state.Hyperspace.PID}/> - 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)} - mounted={this.state.Sia.Mounted} - items={this.state.Sia.DriveLetters} - platform={this.props.platform} - title={'Sia'} - location={this.props.sia.MountLocation} changed={(e) => this.handleMountLocationChanged('Sia', e.target.value)} clicked={this.handleMountUnMount} - pid={this.state.Sia.PID}/> + 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'}/>
); } } diff --git a/src/hoc/RootElem/RootElem.js b/src/hoc/RootElem/RootElem.js new file mode 100644 index 0000000..a40dcf7 --- /dev/null +++ b/src/hoc/RootElem/RootElem.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export default (props) => { + return ( +
+ {props.children} +
+ ) +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index fe9fcd3..9a4c0ce 100644 --- a/src/index.css +++ b/src/index.css @@ -2,22 +2,23 @@ --border_radius: 4px; --control_background: rgba(150, 150, 190, .15); - --control_background_hover: rgba(150, 150, 190, .3); + --control_background_hover: rgba(150, 150, 190, .35); --control_border: 1px solid rgba(70, 70, 70, 0.9); --control_box_shadow: 1px 1px 1px black; - --control_transparent_background: rgba(60, 60, 70, 0.4); - --control_dark_transparent_background: rgba(60, 60, 70, 0.4); + --control_transparent_background: rgba(40, 40, 55, 0.45); + --control_dark_transparent_background: rgba(15, 15, 15, 0.8); --text_color: rgba(200, 205, 220, 0.7); --text_color_hover: rgba(200, 205, 220, 0.7); - --heading_text_color: rgba(194, 217, 255, 0.6); - --heading_other_text_color: rgba(200, 205, 220, 0.7); + --text_color_error: rgba(203, 120, 120, 0.8); + --heading_text_color: rgba(161, 190, 219, 0.7); + --heading_other_text_color: var(--heading_text_color); --text_color_transition: color 0.3s; } * { font-family: 'Nunito', sans-serif; - font-size: 15px; + font-size: 5vh; } *::-moz-focus-inner { @@ -61,3 +62,27 @@ h1 { h2, h3 { color: var(--heading_other_text_color); } + +p { + margin: 0; + padding: 0; + font-weight: normal; + color: var(--text_color); +} + +.scrollable-content { + overflow-x: hidden; + overflow-y: scroll; +} + +.scrollable-content, ::-webkit-scrollbar { + width: 10px; +} + +.scrollable-content, ::-webkit-scrollbar * { + background: transparent; +} + +.scrollable-content, ::-webkit-scrollbar-thumb { + background: rgba(90, 90, 90, 0.6) !important; +} diff --git a/src/index.js b/src/index.js index 46a1b71..a7d5c00 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,20 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; - import App from './App'; -import registerServiceWorker from './registerServiceWorker'; import packageJson from '../package.json'; +import registerServiceWorker from './registerServiceWorker'; + +const Constants = require('./constants'); + +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) => { + ReactDOM.render(, document.getElementById('root')); + registerServiceWorker(); + }); + ipcRenderer.send(Constants.IPC_Get_Platform); + } +} -ReactDOM.render(, document.getElementById('root')); -registerServiceWorker();