diff --git a/electron.js b/electron.js
index 214dbe1..8fafc22 100644
--- a/electron.js
+++ b/electron.js
@@ -143,6 +143,12 @@ app.on('ready', () => {
}
]);
Menu.setApplicationMenu(mainMenu);
+
+ app.on('open-url', (event, url) => {
+ event.preventDefault();
+
+ win.webContents.send('open-url', url);
+ });
});
// Quit when all windows are closed.
diff --git a/src/App.js b/src/App.js
index 4c2c0dd..060d60f 100644
--- a/src/App.js
+++ b/src/App.js
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { hideModal, showModal } from './lib/actions';
import CreateModal from './CreateModal';
+import Extensions from './Extensions';
import Header from './Header';
import Installer from './Installer';
import MachineList from './MachineList';
@@ -33,6 +34,10 @@ class App extends Component {
modalComponent = ;
break;
+ case 'extensions':
+ modalComponent = ;
+ break;
+
default:
// No-op
break;
diff --git a/src/Extensions.css b/src/Extensions.css
new file mode 100644
index 0000000..1ae1c30
--- /dev/null
+++ b/src/Extensions.css
@@ -0,0 +1,68 @@
+.Extensions {
+ display: flex;
+ flex-direction: column;
+}
+.Extensions .Header .search {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ color: #fff;
+ font-size: 1rem;
+ padding: 0.3rem 0.4rem;
+}
+.Extensions .Header .search::-webkit-input-placeholder {
+ color: rgba(255, 255, 255, 0.75);
+}
+.Extensions .Header .search:focus {
+ outline: 0;
+ border-color: rgba(255, 255, 255, 0.5);
+}
+.extensions-list {
+ list-style: none;
+ padding: 0.5rem 0.5rem 0;
+ margin: 0;
+
+ overflow-y: scroll;
+ flex-grow: 1;
+
+ font-size: 14px;
+ display: flex;
+ flex-direction: column;
+}
+.extensions-list li {
+ border: 1px solid #ccc;
+ border-radius: 0.3em;
+ padding: 1em;
+ margin-bottom: 1em;
+ flex-shrink: 0;
+ transition: transform 100ms, opacity 130ms;
+ display: flex;
+}
+.extensions-list li > :first-child {
+ flex-grow: 1;
+}
+.extensions-list li.no-match {
+ // transform: scale(0.5, 0.5);
+ opacity: 0;
+ order: 5;
+}
+.extensions-list h2 {
+ margin: 0 0 0.3em;
+ font-size: 1.3em;
+}
+.extensions-list p {
+ margin: 0 0 0.6em;
+}
+.extensions-list .author {
+ color: #888;
+ margin-bottom: 0;
+}
+.extensions-list .author img {
+ width: 28px;
+ height: 28px;
+ background: var(--color-primary);
+ padding: 2px;
+ vertical-align: middle;
+ margin-right: 0.5em;
+ border-radius: 4px;
+}
diff --git a/src/Extensions.js b/src/Extensions.js
new file mode 100644
index 0000000..393fbd0
--- /dev/null
+++ b/src/Extensions.js
@@ -0,0 +1,94 @@
+import React from 'react';
+
+import Button from './Button';
+import Header from './Header';
+
+import './Extensions.css';
+
+const EXTENSIONS_INDEX = 'http://beta.chassis.io/extensions.json';
+
+export default class Extensions extends React.Component {
+ constructor( props ) {
+ super( props );
+
+ this.state = {
+ available: [],
+ loading: true,
+ search: '',
+ };
+
+ this.loadExtensions();
+ }
+
+ loadExtensions() {
+ fetch( EXTENSIONS_INDEX )
+ .then( resp => resp.json() )
+ .then( available => this.setState({ available, loading: false }) );
+ }
+
+ render() {
+ const { onDismiss } = this.props;
+ const { available, loading, search } = this.state;
+
+ const withAuthors = available.map(item => {
+ const author = {
+ name: 'Chassis',
+ avatar: '/logo.png',
+ };
+
+ return { ...item, author };
+ })
+
+ const filtered = search === '' ? withAuthors : withAuthors.map( item => {
+ const searchable = item.name + ' ' + item.description;
+ console.log( searchable.toLowerCase().indexOf( search.toLowerCase() ) !== -1 );
+ return { ...item, matched: searchable.toLowerCase().indexOf( search.toLowerCase() ) !== -1 };
+ });
+ filtered.map( item => console.log( item.name, search && ! item.matched ) );
+
+ return
+
+
+ { loading ?
+
Loading…
+ :
+
+ { filtered.map( extension =>
+ -
+
+
{ extension.name }
+
{ extension.description }
+
+
+ { extension.author.name }
+
+
+
+
+
+
+ ) }
+
+ }
+
+ }
+}
diff --git a/src/lib/configure.js b/src/lib/configure.js
index fa14d9c..f08a96d 100644
--- a/src/lib/configure.js
+++ b/src/lib/configure.js
@@ -2,11 +2,13 @@
* Setup for global handlers.
*/
import ansiHTML from 'ansi-html';
+import { ipcRenderer } from 'electron';
import which from 'which';
import * as actions from './actions';
import { loadAllConfig } from './actions/loadConfig';
import Keys from './keys';
+import openInternal from './openInternal';
// Refresh every 10 seconds.
const REFRESH_INTERVAL = 10000;
@@ -38,4 +40,6 @@ export default store => {
// Refresh configuration.
store.dispatch(loadAllConfig());
}
+
+ ipcRenderer.on('open-url', (event, url) => openInternal(store)(url));
};
diff --git a/src/lib/openInternal.js b/src/lib/openInternal.js
new file mode 100644
index 0000000..9e95bd8
--- /dev/null
+++ b/src/lib/openInternal.js
@@ -0,0 +1,27 @@
+import url from 'url';
+
+import { showModal } from './actions';
+
+const parseURL = internalURL => {
+ // Parse URL into parts.
+ const parsed = url.parse( internalURL, true );
+ if ( parsed.protocol !== 'chassis:' ) {
+ return null;
+ }
+
+ return parsed;
+};
+
+export default store => url => {
+ const parsed = parseURL( url );
+
+ switch ( parsed.host ) {
+ case 'install-extension':
+ store.dispatch( showModal( 'extensions' ) );
+ break;
+
+ default:
+ console.log( 'unknown action' );
+ break;
+ }
+};