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 ( +
      + {items} +
    + ); + + } + +}); 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 ( +
    + {header} + +
    + ); + + }, + + 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');