diff --git a/css/style.css b/css/style.css
index 6739667..fe28299 100644
--- a/css/style.css
+++ b/css/style.css
@@ -103,6 +103,24 @@ html, body {
color: white;
}
+/* Edit View */
+
+.edit ul.desktop-apps {
+ list-style: none;
+ padding: 0;
+}
+
+.edit li.desktop-app {
+ height: 30px;
+}
+
+.edit img.desktop-app-icon {
+ height: 30px;
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 10px;
+}
+
/* Animations */
.pulse {
diff --git a/js/app.jsx b/js/app.jsx
index 6d76423..43715eb 100644
--- a/js/app.jsx
+++ b/js/app.jsx
@@ -1,10 +1,8 @@
var React = require('react');
var minimist = require('minimist');
var gui = global.window.require('nw.gui');
-var Util = require('./util');
-var CategoryHeader = require('./components/category-header.jsx');
-var AppList = require('./components/app-list.jsx');
-var AnimateMixin = require('./mixins/animate');
+var LauncherView = require('./components/launcher-view.jsx');
+var EditView = require('./components/edit-view.jsx');
// Internal constants
var DEFAULT_PROFILE = './default-profile.json';
@@ -14,134 +12,28 @@ var PROCESS_OPTS = minimist(gui.App.argv);
// Main component
var App = React.createClass({
- mixins: [AnimateMixin],
-
getInitialState: function() {
return {
- currentItemPath: '',
- currentItem: null
+ profilePath: PROCESS_OPTS.profile,
+ editMode: PROCESS_OPTS.edit || false
};
},
- componentDidMount: function() {
-
- // Load profile on component mount
- Util.System.loadJSONFile(PROCESS_OPTS.profile || DEFAULT_PROFILE)
- .then(function(profile) {
- this.setState({ profile: profile, currentItem: profile, currentItemPath: '' });
- }.bind(this))
- ;
-
- },
-
render: function() {
- var currentItem = this.state.currentItem;
- var items = currentItem ? currentItem.items : [];
- var currentItemPath = this.state.currentItemPath;
-
- var header = currentItemPath !== '' ?
- ( ) :
- null
+ var view = this.state.editMode ?
+ :
+
;
return (
-
- {header}
-
+
+ {view}
);
},
- onBackClick: function(itemPath) {
-
- var parentPath = this._normalizeItemPath(itemPath).slice(0, -1);
- var parentItem = this._getItemByPath(parentPath);
-
- this.play(this.refs.appList, 'slide-out-right 250ms ease-in-out')
- .then(function() {
- this.setState({currentItem: parentItem, currentItemPath: parentPath.join('.')});
- return this.play(this.refs.appList, 'slide-in-left 250ms ease-in-out');
- }.bind(this))
- ;
-
- },
-
- onItemClick: function(evt, itemPath, item) {
-
- if(item.exec) {
-
- console.info('Launching application "'+item.exec+'"...');
- evt.currentTarget.classList.add('pulse');
-
- Util.System.runApp(item.exec)
- .then(function() {
- evt.currentTarget.classList.remove('pulse');
- })
- .catch(function(err) {
- evt.currentTarget.classList.remove('pulse');
- })
- ;
-
- } else {
- this.play(this.refs.appList, 'slide-out-left 250ms ease-in-out')
- .then(function() {
- this.setState({ currentItemPath: itemPath, currentItem: item });
- return this.play(this.refs.appList, 'slide-in-right 250ms ease-in-out');
- }.bind(this))
- ;
- }
-
- },
-
- _getItemByPath: function(itemPath, rootItem) {
-
- rootItem = rootItem || this.state.profile;
- itemPath = this._normalizeItemPath(itemPath);
-
- var itemIndex = itemPath[0];
-
- if(itemIndex === undefined) {
- return rootItem;
- }
-
- if(!('items' in rootItem)) {
- return undefined;
- }
-
- var subItem = rootItem.items[itemIndex];
-
- if(itemPath.length === 0) {
- return subItem;
- }
-
- return this._getItemByPath(itemPath.slice(1), subItem);
-
- },
-
- _normalizeItemPath: function(itemPath) {
-
- if( Array.isArray(itemPath) ) return itemPath;
-
- if((typeof itemPath === 'string' && itemPath.length === 0) || !itemPath) return [];
-
- return itemPath.split('.').reduce(function(arr, index) {
- if(index !== '') {
- arr.push(+index);
- }
- return arr;
- }, []);
-
- }
-
});
-var rootEl = document.getElementById('pitaya');
-React.render(
, rootEl);
+React.render(
, document.body);
diff --git a/js/components/desktop-app-item.jsx b/js/components/desktop-app-item.jsx
new file mode 100644
index 0000000..60ff87f
--- /dev/null
+++ b/js/components/desktop-app-item.jsx
@@ -0,0 +1,46 @@
+var React = require('react');
+var Util = require('../util');
+
+module.exports = React.createClass({
+
+ getInitialState: function() {
+ return { icon: '' };
+ },
+
+ render: function() {
+
+ var desktopEntry = this.props.desktopEntry;
+ var label = desktopEntry.Name;
+ var category = desktopEntry.Categories;
+
+ // Search for best icon
+ var icon = '';
+
+ if(!this.state.icon) {
+ this._findIcon(desktopEntry.Icon);
+ } else {
+ icon = this.state.icon;
+ }
+
+ return (
+
+
+ {label}
+
+ );
+
+ },
+
+ _findIcon: function(iconPath) {
+
+ var desktopEntry = this.props.desktopEntry;
+
+ Util.DesktopApps.findIcon(iconPath)
+ .then(function(iconPath) {
+ this.setState({ icon: iconPath });
+ }.bind(this))
+ ;
+
+ }
+
+});
diff --git a/js/components/desktop-app-list.jsx b/js/components/desktop-app-list.jsx
new file mode 100644
index 0000000..3984efc
--- /dev/null
+++ b/js/components/desktop-app-list.jsx
@@ -0,0 +1,37 @@
+var React = require('react');
+var Util = require('../util');
+var DesktopAppItem = require('./desktop-app-item.jsx');
+
+module.exports = React.createClass({
+
+ getInitialState: function() {
+ return {
+ desktopFiles: []
+ };
+ },
+
+ componentDidMount: function() {
+ // Load system desktop apps
+ Util.DesktopApps.loadAllDesktopFiles('/usr/share/applications')
+ .then(function(desktopFiles) {
+ this.setState({ desktopFiles: desktopFiles });
+ }.bind(this))
+ ;
+ },
+
+ render: function() {
+
+ var items = this.state.desktopFiles.map(function(desktopFile, i) {
+ var desktopEntry = desktopFile.content['Desktop Entry'];
+ return
;
+ });
+
+ return (
+
+ );
+
+ }
+
+});
diff --git a/js/components/edit-view.jsx b/js/components/edit-view.jsx
new file mode 100644
index 0000000..96922ca
--- /dev/null
+++ b/js/components/edit-view.jsx
@@ -0,0 +1,16 @@
+var React = require('react');
+var DesktopAppList = require('./desktop-app-list.jsx');
+
+module.exports = React.createClass({
+
+ render: function() {
+
+ return (
+
+
+
+ );
+
+ }
+
+});
diff --git a/js/components/launcher-view.jsx b/js/components/launcher-view.jsx
new file mode 100644
index 0000000..358e03b
--- /dev/null
+++ b/js/components/launcher-view.jsx
@@ -0,0 +1,140 @@
+var React = require('react');
+var Util = require('../util');
+var CategoryHeader = require('./category-header.jsx');
+var AppList = require('./app-list.jsx');
+var AnimateMixin = require('./mixins/animate');
+
+module.exports = React.createClass({
+
+ mixins: [AnimateMixin],
+
+ propTypes: {
+ profilePath: React.PropTypes.string.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ currentItemPath: '',
+ currentItem: null
+ };
+ },
+
+ componentDidMount: function() {
+
+ // Load profile on component mount
+ Util.System.loadJSONFile(this.props.profilePath)
+ .then(function(profile) {
+ this.setState({ profile: profile, currentItem: profile, currentItemPath: '' });
+ }.bind(this))
+ ;
+
+ },
+
+ render: function() {
+
+ var currentItem = this.state.currentItem;
+ var items = currentItem ? currentItem.items : [];
+ var currentItemPath = this.state.currentItemPath;
+
+ var header = currentItemPath !== '' ?
+ (
) :
+ null
+ ;
+
+ return (
+
+ );
+
+ },
+
+ onBackClick: function(itemPath) {
+
+ var parentPath = this._normalizeItemPath(itemPath).slice(0, -1);
+ var parentItem = this._getItemByPath(parentPath);
+
+ this.play(this.refs.appList, 'slide-out-right 250ms ease-in-out')
+ .then(function() {
+ this.setState({currentItem: parentItem, currentItemPath: parentPath.join('.')});
+ return this.play(this.refs.appList, 'slide-in-left 250ms ease-in-out');
+ }.bind(this))
+ ;
+
+ },
+
+ onItemClick: function(evt, itemPath, item) {
+
+ if(item.exec) {
+
+ console.info('Launching application "'+item.exec+'"...');
+ evt.currentTarget.classList.add('pulse');
+
+ Util.System.runApp(item.exec)
+ .then(function() {
+ evt.currentTarget.classList.remove('pulse');
+ })
+ .catch(function(err) {
+ evt.currentTarget.classList.remove('pulse');
+ })
+ ;
+
+ } else {
+ this.play(this.refs.appList, 'slide-out-left 250ms ease-in-out')
+ .then(function() {
+ this.setState({ currentItemPath: itemPath, currentItem: item });
+ return this.play(this.refs.appList, 'slide-in-right 250ms ease-in-out');
+ }.bind(this))
+ ;
+ }
+
+ },
+
+ _getItemByPath: function(itemPath, rootItem) {
+
+ rootItem = rootItem || this.state.profile;
+ itemPath = this._normalizeItemPath(itemPath);
+
+ var itemIndex = itemPath[0];
+
+ if(itemIndex === undefined) {
+ return rootItem;
+ }
+
+ if(!('items' in rootItem)) {
+ return undefined;
+ }
+
+ var subItem = rootItem.items[itemIndex];
+
+ if(itemPath.length === 0) {
+ return subItem;
+ }
+
+ return this._getItemByPath(itemPath.slice(1), subItem);
+
+ },
+
+ _normalizeItemPath: function(itemPath) {
+
+ if( Array.isArray(itemPath) ) return itemPath;
+
+ if((typeof itemPath === 'string' && itemPath.length === 0) || !itemPath) return [];
+
+ return itemPath.split('.').reduce(function(arr, index) {
+ if(index !== '') {
+ arr.push(+index);
+ }
+ return arr;
+ }, []);
+
+ }
+
+});
diff --git a/js/mixins/animate.js b/js/components/mixins/animate.js
similarity index 100%
rename from js/mixins/animate.js
rename to js/components/mixins/animate.js
diff --git a/js/util/desktop-apps.js b/js/util/desktop-apps.js
new file mode 100644
index 0000000..9335985
--- /dev/null
+++ b/js/util/desktop-apps.js
@@ -0,0 +1,96 @@
+var ini = require('ini');
+var glob = require('glob');
+var path = require('path');
+var fs = require('fs');
+
+// Constants
+var ICON_REALPATH_REGEX = /\..+$/;
+
+exports.loadAllDesktopFiles = function(rootDirs) {
+
+ return exports.findAllDesktopFiles(rootDirs)
+ .then(function(filePaths) {
+
+ var promises = filePaths.map(function(path) {
+ return exports.loadDesktopFile(path);
+ });
+
+ return Promise.all(promises)
+ .then(function(contents) {
+ return contents.map(function(content, i) {
+ return { content: content, path: filePaths[i] };
+ });
+ })
+ ;
+
+ })
+ ;
+
+};
+
+exports.findAllDesktopFiles = function(rootDirs) {
+
+ if(!Array.isArray(rootDirs)) {
+ rootDirs = [rootDirs];
+ }
+
+ var promises = rootDirs.map(function(rootDir) {
+
+ var globPath = path.join(rootDir, '**/*.desktop');
+
+ return new Promise(function(resolve, reject) {
+ glob(globPath, function(err, files) {
+ if(err) return reject(err);
+ return resolve(files);
+ });
+ });
+
+ });
+
+ return Promise.all(promises)
+ .then(function(apps) {
+ return uniq(flatten(apps));
+ })
+ ;
+
+};
+
+exports.loadDesktopFile = function(filePath) {
+ return new Promise(function(resolve, reject) {
+ fs.readFile(filePath, 'utf8', function(err, content) {
+ if(err) return reject(err);
+ try {
+ var decoded = ini.decode(content);
+ return resolve(decoded);
+ } catch(err) {
+ return reject(err);
+ }
+ });
+ });
+};
+
+exports.findIcon = function(iconPath) {
+ return new Promise(function(resolve, reject) {
+ if( ICON_REALPATH_REGEX.test(iconPath) ) {
+ return resolve(iconPath);
+ }
+ });
+};
+
+// Array helpers
+
+function flatten(arr) {
+ return arr.reduce(function(result, item) {
+ result = result.concat.apply(result, Array.isArray(item) ? flatten(item) : [item]);
+ return result;
+ }, []);
+}
+
+function uniq(arr) {
+ return arr.reduce(function(result, item) {
+ if(result.indexOf(item) === -1) {
+ result.push(item);
+ }
+ return result;
+ }, []);
+}
diff --git a/js/util/fs.js b/js/util/fs.js
deleted file mode 100644
index 7830b1e..0000000
--- a/js/util/fs.js
+++ /dev/null
@@ -1,104 +0,0 @@
-(function(Pitaya) {
-
- "use strict";
-
- var ini = require('ini');
- var glob = require('glob');
- var path = require('path');
- var fs = require('fs');
-
- var FS = Pitaya.FS = {};
-
- FS.loadAllDesktopFiles = function(rootDirs) {
-
- return FS.findAllDesktopFiles(rootDirs)
- .then(function(filePaths) {
-
- var promises = filePaths.map(function(path) {
- return FS.loadDesktopFile(path);
- });
-
- return Promise.all(promises)
- .then(function(desktopFiles) {
- return desktopFiles.map(function(desktop, i) {
- return {
- path: filePaths[i],
- desktop: desktop
- };
- });
- })
- ;
-
- })
- ;
-
- };
-
- FS.findAllDesktopFiles = function(rootDirs) {
-
- if(!Array.isArray(rootDirs)) {
- rootDirs = [rootDirs];
- }
-
- var promises = rootDirs.map(function(rootDir) {
-
- var globPath = path.join(rootDir, '**/*.desktop');
-
- return new Promise(function(resolve, reject) {
- glob(globPath, function(err, files) {
- if(err) return reject(err);
- return resolve(files);
- });
- });
-
- });
-
- return Promise.all(promises)
- .then(function(apps) {
- return uniq(flatten(apps));
- })
- ;
-
- };
-
- FS.loadDesktopFile = function(filePath) {
- return new Promise(function(resolve, reject) {
- fs.readFile(filePath, 'utf8', function(err, content) {
- if(err) return reject(err);
- try {
- var decoded = ini.decode(content);
- return resolve(decoded);
- } catch(err) {
- return reject(err);
- }
- });
- });
- };
-
- // Array helpers
-
- function flatten(arr) {
- return arr.reduce(function(result, item) {
- result = result.concat.apply(result, Array.isArray(item) ? flatten(item) : [item]);
- return result;
- }, []);
- }
-
- function uniq(arr) {
- return arr.reduce(function(result, item) {
- if(result.indexOf(item) === -1) {
- result.push(item);
- }
- return result;
- }, []);
- }
-
-
-}(Pitaya = global.Pitaya || {}));
-
-
-Pitaya.FS.loadAllDesktopFiles(['/usr/share/applications', '/home/william/.local/share/applications'])
- .then(function(apps) {
- console.log(apps[5])
- })
-;
diff --git a/js/util/index.js b/js/util/index.js
index 13d684c..c9a08fc 100644
--- a/js/util/index.js
+++ b/js/util/index.js
@@ -1 +1,2 @@
exports.System = require('./system');
+exports.DesktopApps = require('./desktop-apps');