diff --git a/CHANGELOG.md b/CHANGELOG.md index 54270ad..b9f3bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 1.2.0 * Goobox S3 support +## 1.1.5 +* \#38: Enhance new repertory release available notification +* Added `FocusTrap` to modals + ## 1.1.4 * \#39: Cleanup old releases and UI upgrades diff --git a/README.md b/README.md index 590091d..3594fd7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Repertory UI -![alt text](https://i.ibb.co/vmZDFK6/repertory-ui-1-1-2.png) +![alt text](https://i.ibb.co/y4bwwhV/repertory-1-1-4.png) ## GUI for [Repertory](https://bitbucket.org/blockstorage/repertory) Repertory allows you to mount Sia and/or ScPrime blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows. @@ -9,10 +9,10 @@ Repertory allows you to mount Sia and/or ScPrime blockchain storage solutions vi * ScPrime >=1.4.1.2 ## Downloads -* **Repertory UI v1.1.2 Linux 64-bit** [](https://pixeldrain.com/u/5i1mA1gb) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.2_linux_x86_64.AppImage) +* **Repertory UI v1.1.4 Linux 64-bit** [](https://pixeldrain.com/u/KfWzWfxp) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.4_linux_x86_64.AppImage) * NOTE: Linux distributions require `fuse` and `libfuse` to be installed. -* **Repertory UI v1.1.2 OS X 64-bit** [](https://pixeldrain.com/u/jEWmNDRX) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.2_mac.dmg) -* **Repertory UI v1.1.3 Windows 64-bit** [](https://pixeldrain.com/u/xyfCGfcM) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.3_win.exe) +* **Repertory UI v1.1.4 OS X 64-bit** [](https://pixeldrain.com/u/68WJfKsz) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.4_mac.dmg) +* **Repertory UI v1.1.4 Windows 64-bit** [](https://pixeldrain.com/u/h5HoAqxF) [](https://bitbucket.org/blockstorage/repertory-ui/downloads/repertory-ui_1.1.4_win.exe) ## Supported Platforms * OS X 64-bit diff --git a/package.json b/package.json index 086517e..4749801 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,16 @@ "author": "scott.e.graves@protonmail.com", "description": "GUI for Repertory - Repertory allows you to mount Sia, Goobox S3 and/or ScPrime blockchain storage solutions via FUSE on Linux/OS X or via WinFSP on Windows.", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.26", - "@fortawesome/free-solid-svg-icons": "^5.12.0", + "@fortawesome/fontawesome-svg-core": "^1.2.27", + "@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/react-fontawesome": "^0.1.8", - "@reduxjs/toolkit": "^1.2.3", + "@reduxjs/toolkit": "^1.2.4", "auto-launch": "^5.0.5", "axios": "^0.19.2", "devtron": "^1.4.0", "electron-debug": "^3.0.1", + "electron-log": "^4.0.6", + "focus-trap-react": "^6.0.0", "font-awesome": "^4.7.0", "node-schedule": "^1.3.2", "randomstring": "^1.1.5", @@ -21,10 +23,10 @@ "react-loader-spinner": "^3.1.5", "react-redux": "^7.1.3", "react-scripts": "3.3.1", - "react-tooltip": "^3.11.4", + "react-tooltip": "^4.0.3", "redux": "^4.0.5", "redux-thunk": "^2.3.0", - "unzipper": "^0.10.7", + "unzipper": "^0.10.8", "winreg": "^1.2.4" }, "devDependencies": { diff --git a/src/App.js b/src/App.js index e2cdd81..c34ca6e 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import InfoDetails from './components/InfoDetails/InfoDetails'; import IPCContainer from './containers/IPCContainer/IPCContainer'; import Loading from './components/UI/Loading/Loading'; import MountItems from './containers/MountItems/MountItems'; +import NewReleases from './components/NewReleases/NewReleases'; import {notifyError} from './redux/actions/error_actions'; import Reboot from './components/Reboot/Reboot'; import ReleaseVersionDisplay from './components/ReleaseVersionDisplay/ReleaseVersionDisplay'; @@ -111,12 +112,26 @@ class App extends IPCContainer { !this.props.DismissDependencies && this.props.AllowMount; + const showNewReleases = !showConfig && + !this.props.DisplayConfirmYesNo && + !showDependencies && + !this.props.DownloadActive && + !this.props.DisplayError && + !this.props.DisplayInfo && + !this.props.InstallActive && + !this.props.RebootRequired && + !this.props.DisplaySelectAppPlatform && + !showUpgrade && + !this.props.DismissNewReleasesAvailable && + (this.props.NewReleasesAvailable.length > 0); + const configDisplay = createModalConditionally(showConfig, ); const confirmDisplay = createModalConditionally(this.props.DisplayConfirmYesNo, ); const dependencyDisplay = createModalConditionally(showDependencies, ); - const downloadDisplay = createModalConditionally(this.props.DownloadActive, ); + const downloadDisplay = createModalConditionally(this.props.DownloadActive, , false, true); const errorDisplay = createModalConditionally(this.props.DisplayError, , true); const infoDisplay = createModalConditionally(this.props.DisplayInfo, , true); + const newReleasesDisplay = createModalConditionally(showNewReleases, ); const rebootDisplay = createModalConditionally(this.props.RebootRequired, ); const selectAppPlatformDisplay = createModalConditionally(this.props.DisplaySelectAppPlatform, ); const upgradeDisplay = createModalConditionally(showUpgrade, ); @@ -175,6 +190,7 @@ class App extends IPCContainer { {mainContent} + {newReleasesDisplay} {selectAppPlatformDisplay} {dependencyDisplay} {upgradeDisplay} @@ -201,12 +217,14 @@ const mapStateToProps = state => { DisplayError: state.error.DisplayError, DisplayInfo: state.error.DisplayInfo, DisplaySelectAppPlatform: state.common.DisplaySelectAppPlatform, + DismissNewReleasesAvailable: state.relver.DismissNewReleasesAvailable, DownloadActive: state.download.DownloadActive, InstallActive: state.install.InstallActive, InstalledVersion: state.relver.InstalledVersion, LocationsLookup: state.relver.LocationsLookup, MissingDependencies: state.install.MissingDependencies, MountsBusy: state.mounts.MountsBusy, + NewReleasesAvailable: state.relver.NewReleasesAvailable, Platform: state.common.Platform, ProviderState: state.mounts.ProviderState, RebootRequired: state.common.RebootRequired, diff --git a/src/components/DependencyList/Dependency/Dependency.js b/src/components/DependencyList/Dependency/Dependency.js index f869874..71bc437 100644 --- a/src/components/DependencyList/Dependency/Dependency.js +++ b/src/components/DependencyList/Dependency/Dependency.js @@ -22,9 +22,9 @@ export default connect(mapStateToProps)(props => { {props.AllowDownload ? - {props.onDownload(); return false;}}>Install : + {props.onDownload(); return false;}}>Install : 'Installing...'} @@ -32,4 +32,4 @@ export default connect(mapStateToProps)(props => { ); -}); \ No newline at end of file +}); diff --git a/src/components/NewReleases/NewRelease/NewRelease.css b/src/components/NewReleases/NewRelease/NewRelease.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/NewReleases/NewRelease/NewRelease.js b/src/components/NewReleases/NewRelease/NewRelease.js new file mode 100644 index 0000000..a8b5de3 --- /dev/null +++ b/src/components/NewReleases/NewRelease/NewRelease.js @@ -0,0 +1,28 @@ +import React from 'react'; +import * as Constants from '../../../constants'; +import Button from '../../UI/Button/Button'; + +export default ({release, lastItem}) => { + return ( +
+

{'[' + Constants.RELEASE_TYPES[release.Release] + '] ' + release.Display }

+ + + + + + + + + {lastItem ? null : } + +
+ + +
+
+ +
+
+ ); +}; diff --git a/src/components/NewReleases/NewReleases.css b/src/components/NewReleases/NewReleases.css new file mode 100644 index 0000000..71777c2 --- /dev/null +++ b/src/components/NewReleases/NewReleases.css @@ -0,0 +1,11 @@ +.NewReleasesHeading { + text-align: center; + margin-bottom: 4px; +} + +.NewReleasesContent { + max-height: 60vh; + min-width: 50vw; + overflow-y: auto; + margin-bottom: var(--default_spacing); +} diff --git a/src/components/NewReleases/NewReleases.js b/src/components/NewReleases/NewReleases.js new file mode 100644 index 0000000..5c4638c --- /dev/null +++ b/src/components/NewReleases/NewReleases.js @@ -0,0 +1,37 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import Box from '../UI/Box/Box'; +import Button from '../UI/Button/Button'; +import NewRelease from './NewRelease/NewRelease'; +import './NewReleases.css'; +import {setDismissNewReleasesAvailable} from '../../redux/actions/release_version_actions'; + +const mapStateToProps = state => { + return { + NewReleasesAvailable: state.relver.NewReleasesAvailable, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + dismissNewReleasesAvailable: () => dispatch(setDismissNewReleasesAvailable(true)), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(props => { + const newReleases = props.NewReleasesAvailable.map((i, idx) => { + return ; + }); + + return ( + +

New Repertory Versions Available

+
+ {newReleases} +
+ +
+ ); +}); diff --git a/src/components/UI/Button/Button.js b/src/components/UI/Button/Button.js index 5435032..ae1b462 100644 --- a/src/components/UI/Button/Button.js +++ b/src/components/UI/Button/Button.js @@ -4,6 +4,7 @@ import './Button.css'; export default props => { return ( diff --git a/src/components/UI/CheckBox/CheckBox.js b/src/components/UI/CheckBox/CheckBox.js index adc0e04..b195cf3 100644 --- a/src/components/UI/CheckBox/CheckBox.js +++ b/src/components/UI/CheckBox/CheckBox.js @@ -6,6 +6,7 @@ export default props => {
); -}; \ No newline at end of file +}; diff --git a/src/components/UI/DropDown/DropDown.js b/src/components/UI/DropDown/DropDown.js index f19cc1d..485465c 100644 --- a/src/components/UI/DropDown/DropDown.js +++ b/src/components/UI/DropDown/DropDown.js @@ -11,6 +11,7 @@ export default props => { return (
this.setState({HostNameOrIp: e.target.value.trim()})} className={'ConfigurationItemInput'} type={'text'} @@ -73,7 +73,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class extends Compon
+ type={'Heading2'}/> this.setState({Port: e.target.value})} @@ -83,7 +83,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class extends Compon
+ type={'Heading2'}/> this.setState({Token: e.target.value})} className={'ConfigurationItemInput'} type={'text'} @@ -114,4 +114,4 @@ export default connect(mapStateToProps, mapDispatchToProps)(class extends Compon
); } -}); \ No newline at end of file +}); diff --git a/src/containers/Configuration/Configuration.js b/src/containers/Configuration/Configuration.js index 2136f49..f8c30e9 100644 --- a/src/containers/Configuration/Configuration.js +++ b/src/containers/Configuration/Configuration.js @@ -163,6 +163,8 @@ class Configuration extends IPCContainer { ObjectLookup: objectLookup, OriginalItemList: itemListCopy, OriginalObjectLookup: objectLookupCopy, + }, () => { + }); } else { this.props.notifyError(arg.data.Error); @@ -262,32 +264,22 @@ class Configuration extends IPCContainer { ); } - const configurationItems = this.state.ItemList - .map((k, i) => { - return ( - ((!this.state.IsRemoteMount || !k.hide_remote) && (!k.advanced || (this.state.ShowAdvanced && k.advanced))) ? - this.handleItemChanged(e, i)} - grouping={'Settings'} - items={this.state.Template[k.label].items} - key={i} - label={k.label} - template={this.state.Template[k.label]} - value={k.value}/> : - null) - }); + let autoFocus = true; let objectItems = []; for (const key of Object.keys(this.state.ObjectLookup)) { objectItems.push((
-

{key}

+

{key}

{ this.state.ObjectLookup[key].map((k, i) => { + const shouldFocus = autoFocus; + autoFocus = false; return ( (!k.advanced || (this.state.ShowAdvanced && k.advanced && !k.remote) || this.showRemoteConfigItem(k, this.state.ObjectLookup[key])) ? this.handleObjectItemChanged(e, key, i)} grouping={key} items={this.state.Template[key].template[k.label].items} @@ -304,13 +296,33 @@ class Configuration extends IPCContainer { )); } + const configurationItems = this.state.ItemList + .map((k, i) => { + const shouldFocus = autoFocus; + autoFocus = false; + return ( + ((!this.state.IsRemoteMount || !k.hide_remote) && (!k.advanced || (this.state.ShowAdvanced && k.advanced))) ? + this.handleItemChanged(e, i)} + grouping={'Settings'} + items={this.state.Template[k.label].items} + key={i} + label={k.label} + template={this.state.Template[k.label]} + value={k.value}/> : + null + ); + }); + return (
{confirmSave}
- X + X

{( this.props.DisplayRemoteConfiguration ? @@ -318,7 +330,7 @@ class Configuration extends IPCContainer { this.props.DisplayConfiguration) + ' Configuration'}

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

Settings

: null} + {(configurationItems.length > 0) ?

Settings

: null} {configurationItems}
diff --git a/src/containers/Configuration/ConfigurationItem/ConfigurationItem.js b/src/containers/Configuration/ConfigurationItem/ConfigurationItem.js index 5a4d08a..d46e9e5 100644 --- a/src/containers/Configuration/ConfigurationItem/ConfigurationItem.js +++ b/src/containers/Configuration/ConfigurationItem/ConfigurationItem.js @@ -18,7 +18,7 @@ export default connect(null, mapDispatchToProps)(props => { const handleChanged = (e) => { const target = e.target; if (target.type === 'checkbox') { - target.value = e.target.checked ? "true" : "false"; + target.value = e.target.checked ? 'true' : 'false'; } props.changed(target); }; @@ -30,49 +30,54 @@ export default connect(null, mapDispatchToProps)(props => { props.notifyInfo(props.label, description); }; - infoDisplay = {displayInfo(); return false;}}>; + infoDisplay = {displayInfo(); return false;}}>; } - + let data; switch (props.template.type) { - case "bool": + case 'bool': data = ; + disabled={props.readOnly} + autoFocus={props.autoFocus}/>; break; - case "double": + case 'double': data = handleChanged(e)} - step={"0.01"} + step={'0.01'} className={'ConfigurationItemInput'} type={'number'} value={parseFloat(props.value).toFixed(2)}/>; break; - case "list": + case 'list': data = ; break; - case "string": + case 'string': data = handleChanged(e)} + autoFocus={props.autoFocus} className={'ConfigurationItemInput'} disabled={props.readOnly} type={'text'} value={props.value}/>; break; - case "uint8": + case 'uint8': data = handleChanged(e)} className={'ConfigurationItemInput'} @@ -80,9 +85,10 @@ export default connect(null, mapDispatchToProps)(props => { value={props.value}/>; break; - case "uint16": + case 'uint16': data = handleChanged(e)} className={'ConfigurationItemInput'} @@ -90,9 +96,10 @@ export default connect(null, mapDispatchToProps)(props => { value={props.value}/>; break; - case "uint32": + case 'uint32': data = handleChanged(e)} className={'ConfigurationItemInput'} @@ -100,9 +107,10 @@ export default connect(null, mapDispatchToProps)(props => { value={props.value}/>; break; - case "uint64": + case 'uint64': data = handleChanged(e)} className={'ConfigurationItemInput'} @@ -129,4 +137,4 @@ export default connect(null, mapDispatchToProps)(props => {
); -}); \ No newline at end of file +}); diff --git a/src/containers/MountItems/MountItem/MountItem.js b/src/containers/MountItems/MountItem/MountItem.js index 3572b47..21af275 100644 --- a/src/containers/MountItems/MountItem/MountItem.js +++ b/src/containers/MountItems/MountItem/MountItem.js @@ -152,8 +152,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(props => { } }; removeControl = ( - dimensions.columns - 6} - href={void(0)} + dimensions.columns - 6} onClick={handleRemoveMount} row={secondRow + 3} style={removeStyle}> @@ -169,7 +169,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(props => { col={configButton ? 6 : 0} rowSpan={5} text={props.remote ? props.provider.substr(6) : props.provider} - type={'Heading1'}/> + type={'Heading2'}/> {inputControls} {actionsDisplay} {autoMountControl} diff --git a/src/index.css b/src/index.css index ce1f473..42eae46 100644 --- a/src/index.css +++ b/src/index.css @@ -8,11 +8,11 @@ --control_transparent_background: rgba(10, 10, 16, 0.5); --control_dark_transparent_background: rgba(10, 10, 16, 0.7); - --text_color: rgba(200, 200, 240, 0.7); - --text_color_hover: rgba(200, 200, 225, 0.7); - --text_color_error: rgba(203, 120, 120, 0.7); - --heading_text_color: rgba(132, 160, 230, 0.7); - --heading_other_text_color: var(--heading_text_color); + --text_color: rgba(200, 200, 240, 0.65); + --text_color_hover: rgba(200, 200, 225, 0.65); + --text_color_error: rgba(203, 120, 120, 0.8); + --heading_text_color: rgba(132, 160, 230, 0.8); + --heading_other_text_color: rgba(132, 160, 230, 0.65); --text_color_transition: color 0.3s; --default_font_size: 14px; @@ -30,6 +30,9 @@ a { outline: 0; + color: var(--text_color); + text-decoration: none; + font-weight: bold; } html, body { diff --git a/src/redux/actions/release_version_actions.js b/src/redux/actions/release_version_actions.js index 7ce8b5c..e570313 100644 --- a/src/redux/actions/release_version_actions.js +++ b/src/redux/actions/release_version_actions.js @@ -13,7 +13,10 @@ import { setDismissDependencies } from './install_actions'; import {unmountAll} from './mount_actions'; -import {getIPCRenderer} from '../../utils'; +import { + getIPCRenderer, + getNewReleases +} from '../../utils'; export const CLEAR_UI_UPGRADE = 'relver/clearUIUpgrade'; export const clearUIUpgrade = () => { @@ -123,11 +126,23 @@ export const loadReleases = () => { ...response.data.Locations[appPlatform], }; + const storedReleases = localStorage.getItem('releases'); + let newReleases = []; + if (storedReleases && (storedReleases.length > 0)) { + newReleases = getNewReleases(JSON.parse(storedReleases).VersionLookup, versionLookup); + } + localStorage.setItem('releases', JSON.stringify({ LocationsLookup: locationsLookup, VersionLookup: versionLookup })); dispatchActions(locationsLookup, versionLookup); + + dispatch(setNewReleasesAvailable(newReleases)); + if (getState().relver.NewReleasesAvailable.length > 0) { + localStorage.setItem('previous_releases', storedReleases); + dispatch(showWindow()); + } }).catch(error => { const releases = localStorage.getItem('releases'); if (releases && (releases.length > 0)) { @@ -174,8 +189,10 @@ export const setActiveRelease = (release, version) => { }; export const setAllowDismissDependencies = createAction('relver/setAllowDismissDependencies'); +export const setDismissNewReleasesAvailable = createAction('relver/setDismissNewReleasesAvailable'); export const setDismissUIUpgrade = createAction('relver/setDismissUIUpgrade'); export const setInstalledVersion = createAction('relver/setInstalledVersion'); +export const setNewReleasesAvailable = createAction('relver/setNewReleasesAvailable'); export const SET_RELEASE_DATA = 'relver/setReleaseData'; export const setReleaseData = (locationsLookup, versionLookup)=> { diff --git a/src/redux/reducers/release_version_reducer.js b/src/redux/reducers/release_version_reducer.js index 56278ac..52066ec 100644 --- a/src/redux/reducers/release_version_reducer.js +++ b/src/redux/reducers/release_version_reducer.js @@ -15,8 +15,10 @@ const versionLookup = Constants.RELEASE_TYPES.map(k=> { export const releaseVersionReducer = createReducer({ AllowDismissDependencies: false, + DismissNewReleasesAvailable: true, InstalledVersion: 'none', LocationsLookup: {}, + NewReleasesAvailable: [], Release: 0, ReleaseDefault: 0, ReleaseUpgradeAvailable: false, @@ -49,6 +51,12 @@ export const releaseVersionReducer = createReducer({ AllowDismissDependencies: action.payload, }; }, + [Actions.setDismissNewReleasesAvailable]: (state, action) => { + return { + ...state, + DismissNewReleasesAvailable: action.payload, + }; + }, [Actions.setDismissUIUpgrade]: (state, action) => { return { ...state, @@ -61,6 +69,13 @@ export const releaseVersionReducer = createReducer({ InstalledVersion: action.payload, } }, + [Actions.setNewReleasesAvailable]: (state, action) => { + return { + ...state, + DismissNewReleasesAvailable: false, + NewReleasesAvailable: action.payload, + }; + }, [Actions.SET_RELEASE_DATA]: (state, action) => { return { ...state, diff --git a/src/utils.js b/src/utils.js index b263338..47d29de 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,8 +6,8 @@ const ipcRenderer = (!process.versions.hasOwnProperty('electron') && window && w window.require('electron').ipcRenderer : null; -export const createModalConditionally = (condition, jsx, critical) => { - const modalProps = {critical: critical}; +export const createModalConditionally = (condition, jsx, critical, disableFocusTrap) => { + const modalProps = {critical: critical, disableFocusTrap: disableFocusTrap}; return condition ? ({jsx}) : null; }; @@ -20,6 +20,37 @@ export const getIPCRenderer = () => { return ipcRenderer; }; +export const getNewReleases = (existingReleases, newReleases) => { + const ret = []; + + /*existingReleases = Constants.RELEASE_TYPES.reduce((map, release) => { + map[release] = []; + return map; + }, {});*/ + + if (existingReleases && newReleases) { + Constants.RELEASE_TYPES.forEach(release => { + newReleases[release] + .filter(version => !existingReleases[release].includes(version) && (version !== 'unavailable')) + .forEach(version => { + ret.splice(0, 0, { + Display: version, + Release: Constants.RELEASE_TYPES.indexOf(release), + Version: newReleases[release].indexOf(version), + }); + }); + }); + } + + ret.push({ + Display: '1.1.1', + Release: 1, + Version: 2, + }); + + return ret; +}; + export const getSelectedVersionFromState = state => { return (state.relver.Version === -1) ? 'unavailable' :