diff --git a/README.md b/README.md index eb4ef0b..57238a9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pitaya -Lanceur d'application pour Linux +Lanceur d'application pour GNU/Linux ## Démarrer avec les sources @@ -12,7 +12,7 @@ Lanceur d'application pour Linux ### Initialisation du projet ``` -git clone +git clone https://forge.cadoles.com/wpetit/pitaya.git cd pitaya git checkout develop npm install @@ -23,6 +23,7 @@ npm start ``` --profile= Chemin vers le fichier de profil à charger dans l'application. +--edit Ouvrir l'application en mode édition. ``` ### Passer des options en développement @@ -48,3 +49,7 @@ Un dossier `pitaya---` sera créé dans le répertoire `. ## Comment contribuer Ce projet utilise la méthodologie [Git Flow](http://nvie.com/posts/a-successful-git-branching-model/). + +## Licence + +GPLv3 diff --git a/index.html b/index.html index fc44a91..aad2c75 100644 --- a/index.html +++ b/index.html @@ -7,52 +7,19 @@
- - - - - - - - - + - - - - - diff --git a/js/app.js b/js/app.js deleted file mode 100644 index 063fe35..0000000 --- a/js/app.js +++ /dev/null @@ -1,242 +0,0 @@ -(function(Pitaya, window) { - - "use strict"; - - // Load dependencies - var path = require('path'); - var fs = require('fs'); - var Handlebars = require('handlebars'); - var cp = require("child_process"); - var gui = require('nw.gui'); - var minimist = require('minimist'); - - // Load templates... - var launcherViewTpl = Handlebars.compile(Pitaya.DOM.select('#launcher-view-tpl').innerHTML); - - // ... and partials - Handlebars.registerPartial('itemListTpl', Pitaya.DOM.select('#items-list-tpl').innerHTML); - Handlebars.registerPartial('itemTpl', Pitaya.DOM.select('#item-tpl').innerHTML); - - // Internal constants - var DEFAULT_PROFILE = './default-profile.json'; - - /** - * Start the app - * - * @param rootEl The application root element selector - * @return Pitaya - */ - Pitaya.start = function(rootEl) { - - Pitaya._opts = minimist(gui.App.argv); - Pitaya._rootEl = Pitaya.DOM.select(rootEl); - Pitaya._initListeners(); - - var profilePath = Pitaya._opts.profile || DEFAULT_PROFILE; - - return Pitaya.loadProfile(profilePath) - .then(function() { - return Pitaya; - }) - .catch(Pitaya._onError) - ; - - }; - - /** - * Load a profile file and render the application - * - * @param profilePath The path of the profile file - * @return Promise - */ - Pitaya.loadProfile = function(profilePath) { - return Pitaya._loadJSONFile(profilePath) - .then(function(profile) { - Pitaya._profile = profile; - Pitaya.renderLauncherView(); - return profile; - }) - ; - }; - - /** - * Update the application view - * - * @return Pitaya - */ - Pitaya.renderLauncherView = function(currentItemPath) { - - currentItemPath = Pitaya._normalizeItemPath(currentItemPath); - var rootEl = Pitaya._rootEl; - var currentItem = Pitaya._getItemByPath(currentItemPath); - - var data = { - currentItemPath: currentItemPath.join('.'), - currentItem: currentItem, - isRoot: currentItemPath.length === 0 - }; - - rootEl.innerHTML = launcherViewTpl(data); - - }; - - - /** - * Initialize DOM event listeners - * @private - */ - Pitaya._initListeners = function() { - var rootEl = Pitaya._rootEl; - rootEl.addEventListener('click', Pitaya._onItemClick); - rootEl.addEventListener('click', Pitaya._onGoBackClick); - }; - - /** - * App item click handler - * @private - */ - Pitaya._onItemClick = function(evt) { - - var appItemEl = evt.srcElement.matches( '.app-item') ? evt.srcElement : - Pitaya.DOM.getClosestAncestor(evt.srcElement, '.app-item') - ; - - if( !appItemEl ) return; - - var itemPath = appItemEl.dataset.itemPath; - var item = Pitaya._getItemByPath(itemPath); - - if(!item) return; - - if('items' in item) { - var rootEl = Pitaya._rootEl; - Pitaya.Anim.play(rootEl, 'slide-out-left 250ms ease-in-out') - .then(function() { - Pitaya.renderLauncherView(itemPath); - return Pitaya.Anim.play(rootEl, 'slide-in-right 250ms ease-in-out'); - }) - ; - } - - if(item.exec) { - - console.info('Launching application "'+item.exec+'"...'); - appItemEl.classList.add('pulse'); - - Pitaya._runApp(item.exec) - .then(function() { - appItemEl.classList.remove('pulse'); - }) - .catch(function(err) { - Pitaya._onError(err); - appItemEl.classList.remove('pulse'); - }) - ; - - } - - }; - - /** - * GoBack button click handler - * @private - */ - Pitaya._onGoBackClick = function(evt) { - - var goBackEl = evt.srcElement.matches( '.goback') ? evt.srcElement : - Pitaya.DOM.getClosestAncestor(evt.srcElement, '.goback') - ; - - if(!goBackEl) return; - - var currentItemPath = goBackEl.dataset.itemPath; - var parentItemPath = Pitaya._normalizeItemPath(currentItemPath); - - parentItemPath.pop(); - - var rootEl = Pitaya._rootEl; - Pitaya.Anim.play(rootEl, 'slide-out-right 250ms ease-in-out') - .then(function() { - Pitaya.renderLauncherView(parentItemPath); - return Pitaya.Anim.play(rootEl, 'slide-in-left 250ms ease-in-out'); - }) - ; - - }; - - Pitaya._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; - }, []); - - }; - - Pitaya._getItemByPath = function(itemPath, rootItem) { - - rootItem = rootItem || Pitaya._profile; - itemPath = Pitaya._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 Pitaya._getItemByPath(itemPath.slice(1), subItem); - - }; - - Pitaya._runApp = function(execPath) { - return new Promise(function(resolve, reject) { - cp.exec(execPath, function(err) { - if(err) return reject(err); - return resolve(); - }); - }); - }; - - /** - * Load a JSON file - * - * @private - * @param filePath The path of the json file - * @return Promise - */ - Pitaya._loadJSONFile = function(filePath) { - return new Promise(function(resolve, reject) { - fs.readFile(filePath, 'utf8', function(err, fileContent) { - if(err) return reject(err); - try { - var json = JSON.parse(fileContent); - return resolve(json); - } catch(err) { - return reject(err); - } - }); - }); - }; - - - Pitaya._onError = function(err) { - console.error(err.stack ? err.stack : err); - }; - -}(window.Pitaya = window.Pitaya || {}, window)); diff --git a/js/app.jsx b/js/app.jsx new file mode 100644 index 0000000..6d76423 --- /dev/null +++ b/js/app.jsx @@ -0,0 +1,147 @@ +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'); + +// Internal constants +var DEFAULT_PROFILE = './default-profile.json'; +var PROCESS_OPTS = minimist(gui.App.argv); + + +// Main component +var App = React.createClass({ + + mixins: [AnimateMixin], + + getInitialState: function() { + return { + currentItemPath: '', + currentItem: null + }; + }, + + 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 + ; + + 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; + }, []); + + } + +}); + +var rootEl = document.getElementById('pitaya'); +React.render(, rootEl); diff --git a/js/components/app-item.jsx b/js/components/app-item.jsx new file mode 100644 index 0000000..37c58ac --- /dev/null +++ b/js/components/app-item.jsx @@ -0,0 +1,28 @@ +var React = require('react'); + +module.exports = React.createClass({ + + propTypes: { + item: React.PropTypes.object.isRequired, + itemPath: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.arrayOf(React.PropTypes.number) + ]).isRequired, + onItemClick: React.PropTypes.func.isRequired, + }, + + _onItemClick: function(evt) { + evt.preventDefault(); + this.props.onItemClick(evt, this.props.itemPath, this.props.item); + }, + + render: function() { + return ( +
  • + + {this.props.item.label} +
  • + ); + } + +}); diff --git a/js/components/app-list.jsx b/js/components/app-list.jsx new file mode 100644 index 0000000..15b81c2 --- /dev/null +++ b/js/components/app-list.jsx @@ -0,0 +1,33 @@ +var React = require('react'); +var AppItem = require('./app-item.jsx'); + +module.exports = React.createClass({ + + propTypes: { + items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + parentPath: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.arrayOf(React.PropTypes.number) + ]).isRequired, + onItemClick: React.PropTypes.func.isRequired, + }, + + render: function() { + + var parentPath = this.props.parentPath; + var items = (this.props.items).map(function(item, i) { + var path = parentPath+'.'+i; + return ( + + ); + }.bind(this)); + + return ( +
      + {items} +
    + ); + + } + +}); diff --git a/js/components/category-header.jsx b/js/components/category-header.jsx new file mode 100644 index 0000000..74c1f72 --- /dev/null +++ b/js/components/category-header.jsx @@ -0,0 +1,30 @@ +var React = require('react'); + +module.exports = React.createClass({ + + propTypes: { + onBackClick: React.PropTypes.func.isRequired, + itemPath: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.arrayOf(React.PropTypes.number) + ]).isRequired, + item: React.PropTypes.object.isRequired, + }, + + render: function() { + + return ( +
    + + {this.props.item.label} +
    + ); + + }, + + _onBackClick: function(evt) { + evt.preventDefault(); + this.props.onBackClick(this.props.itemPath, this.props.item); + } + +}); diff --git a/js/dom.js b/js/dom.js deleted file mode 100644 index 13107c2..0000000 --- a/js/dom.js +++ /dev/null @@ -1,53 +0,0 @@ -(function(Pitaya, window) { - - "use strict"; - - var DOM = Pitaya.DOM = {}; - - /** - * Select an element in the DOM by its CSS selector - * - * @private - * @param selector The CSS selector - * @return The selected element or null - */ - DOM.select = function(selector) { - return window.document.querySelector(selector); - }; - - /** - * Select all elements in the DOM with a CSS selector - * - * @private - * @param selector The CSS selector - * @return An array of the selected elements (if any) - */ - DOM.selectAll = function(selector) { - return window.document.querySelectorAll(selector); - }; - - /** - * Find the closest ancestor matching the CSS selector - * - * @private - * @param selector The CSS selector - * @return An array of the selected elements (if any) - */ - DOM.getClosestAncestor = function(el, selector) { - - var parent = el.parentElement; - - if(parent) { - if(parent.matches(selector)) { - return parent; - } else { - return DOM.getClosestAncestor(parent, selector); - } - } - - return false; - - }; - - -}(window.Pitaya = window.Pitaya || {}, window)); diff --git a/js/anim.js b/js/mixins/animate.js similarity index 55% rename from js/anim.js rename to js/mixins/animate.js index 44f700c..37cf387 100644 --- a/js/anim.js +++ b/js/mixins/animate.js @@ -1,15 +1,14 @@ -(function(Pitaya, window) { +var Events = { + ANIMATION_END: 'webkitAnimationEnd' +}; - "use strict"; +module.exports = { - var Anim = Pitaya.Anim = {}; - var Events = Anim.Events = { - ANIMATION_END: 'webkitAnimationEnd' - }; - - Anim.play = function(el, animation) { + play: function(component, animation) { return new Promise(function(resolve, reject) { - + + var el = component.getDOMNode(); + el.addEventListener(Events.ANIMATION_END, onAnimEnd, false); el.style.webkitAnimation = animation; @@ -19,6 +18,6 @@ } }); - }; + } -}(window.Pitaya = window.Pitaya || {}, window)); +}; diff --git a/js/util/fs.js b/js/util/fs.js new file mode 100644 index 0000000..7830b1e --- /dev/null +++ b/js/util/fs.js @@ -0,0 +1,104 @@ +(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 new file mode 100644 index 0000000..13d684c --- /dev/null +++ b/js/util/index.js @@ -0,0 +1 @@ +exports.System = require('./system'); diff --git a/js/util/system.js b/js/util/system.js new file mode 100644 index 0000000..6932710 --- /dev/null +++ b/js/util/system.js @@ -0,0 +1,31 @@ +var fs = require('fs'); +var cp = require('child_process'); + +/** + * Load a JSON file + * + * @param filePath The path of the json file + * @return Promise + */ +exports.loadJSONFile = function(filePath) { + return new Promise(function(resolve, reject) { + fs.readFile(filePath, 'utf8', function(err, fileContent) { + if(err) return reject(err); + try { + var json = JSON.parse(fileContent); + return resolve(json); + } catch(err) { + return reject(err); + } + }); + }); +}; + +exports.runApp = function(execPath) { + return new Promise(function(resolve, reject) { + cp.exec(execPath, function(err) { + if(err) return reject(err); + return resolve(); + }); + }); +}; diff --git a/package.json b/package.json index 4ac1fb5..0728334 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "kiosk": false }, "dependencies": { - "handlebars": "^3.0.3", - "minimist": "^1.1.3" + "glob": "^5.0.14", + "ini": "^1.3.4", + "minimist": "^1.1.3", + "node-jsx": "^0.13.3", + "react": "^0.13.3" } }