diff --git a/css/style.css b/css/style.css
index fe28299..3de453b 100644
--- a/css/style.css
+++ b/css/style.css
@@ -48,7 +48,6 @@ html, body {
}
.launcher ul.apps-list {
- display: block;
margin: 0;
padding: 0;
display: flex;
@@ -105,17 +104,27 @@ html, body {
/* Edit View */
+.edit {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
.edit ul.desktop-apps {
list-style: none;
padding: 0;
+ overflow-y: auto;
}
.edit li.desktop-app {
- height: 30px;
+
}
.edit img.desktop-app-icon {
- height: 30px;
+ height: 50px;
+ width: 50px;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
diff --git a/img/hourglass.svg b/img/hourglass.svg
new file mode 100644
index 0000000..5a3e56c
--- /dev/null
+++ b/img/hourglass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/components/app-list.jsx b/js/components/app-list.jsx
index 15b81c2..4c13fed 100644
--- a/js/components/app-list.jsx
+++ b/js/components/app-list.jsx
@@ -4,24 +4,38 @@ var AppItem = require('./app-item.jsx');
module.exports = React.createClass({
propTypes: {
+
+ // The app items to display in the list
items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+
+ // the parent item path
parentPath: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.number)
]).isRequired,
+
+ // Item click handler
onItemClick: React.PropTypes.func.isRequired,
+
},
render: function() {
var parentPath = this.props.parentPath;
+
+ // For each items, we create an AppItem component
var items = (this.props.items).map(function(item, i) {
+
+ // The item path identifier
var path = parentPath+'.'+i;
+
return (
);
+
}.bind(this));
+ // Create the apps list
return (
{items}
diff --git a/js/components/desktop-app-item.jsx b/js/components/desktop-app-item.jsx
index 60ff87f..92d5f47 100644
--- a/js/components/desktop-app-item.jsx
+++ b/js/components/desktop-app-item.jsx
@@ -1,10 +1,21 @@
var React = require('react');
var Util = require('../util');
+var LazyLoad = require('./mixins/lazy-load');
module.exports = React.createClass({
+ mixins: [LazyLoad],
+
getInitialState: function() {
- return { icon: '' };
+ return { icon: 'img/hourglass.svg', loading: false };
+ },
+
+ onInViewport: function() {
+ if(!this.state.loading) {
+ this.setState({ loading: true });
+ var desktopEntry = this.props.desktopEntry;
+ this._findIcon(desktopEntry.Icon);
+ }
},
render: function() {
@@ -13,18 +24,9 @@ module.exports = React.createClass({
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}
);
@@ -33,12 +35,12 @@ module.exports = React.createClass({
_findIcon: function(iconPath) {
- var desktopEntry = this.props.desktopEntry;
+ var self = this;
- Util.DesktopApps.findIcon(iconPath)
+ Util.DesktopApps.findIcon(iconPath || 'application-default-icon')
.then(function(iconPath) {
- this.setState({ icon: iconPath });
- }.bind(this))
+ self.setState({ icon: iconPath });
+ })
;
}
diff --git a/js/components/desktop-app-list.jsx b/js/components/desktop-app-list.jsx
index 3984efc..7a0d7ba 100644
--- a/js/components/desktop-app-list.jsx
+++ b/js/components/desktop-app-list.jsx
@@ -1,6 +1,7 @@
var React = require('react');
var Util = require('../util');
var DesktopAppItem = require('./desktop-app-item.jsx');
+var path = require('path');
module.exports = React.createClass({
@@ -12,6 +13,10 @@ module.exports = React.createClass({
componentDidMount: function() {
// Load system desktop apps
+ var baseDirs = global.process.env.XDG_DATA_DIRS.split(':').map(function(baseDir){
+ return path.join(baseDir, 'applications');
+ });
+
Util.DesktopApps.loadAllDesktopFiles('/usr/share/applications')
.then(function(desktopFiles) {
this.setState({ desktopFiles: desktopFiles });
diff --git a/js/components/mixins/lazy-load.js b/js/components/mixins/lazy-load.js
new file mode 100644
index 0000000..4879829
--- /dev/null
+++ b/js/components/mixins/lazy-load.js
@@ -0,0 +1,55 @@
+var React = require('react');
+
+module.exports = {
+
+ isInViewport: function() {
+
+ var el = React.findDOMNode(this);
+
+ if(!el) return false;
+
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (global.window.innerHeight || global.document.documentElement.clientHeight) && /*or $(window).height() */
+ rect.right <= (global.window.innerWidth || global.document.documentElement.clientWidth) /*or $(window).width() */
+ );
+
+ },
+
+ componentDidMount: function() {
+
+ function _onInViewport(){
+ if( this.isInViewport() ) {
+ this.onInViewport();
+ }
+ }
+
+ var el = React.findDOMNode(this);
+
+ if(typeof this.onInViewport === 'function') {
+ el.parentNode.addEventListener('scroll', debounce(_onInViewport.bind(this), 250));
+ }
+
+ _onInViewport.call(this);
+
+ }
+
+};
+
+function debounce(func, wait, immediate) {
+ var timeout;
+ return function() {
+ var context = this, args = arguments;
+ var later = function() {
+ timeout = null;
+ if (!immediate) func.apply(context, args);
+ };
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(context, args);
+ };
+}
diff --git a/js/util/desktop-apps.js b/js/util/desktop-apps.js
index 9335985..b2cc218 100644
--- a/js/util/desktop-apps.js
+++ b/js/util/desktop-apps.js
@@ -1,11 +1,17 @@
-var ini = require('ini');
-var glob = require('glob');
var path = require('path');
-var fs = require('fs');
+var System = require('./system');
+var debug = require('debug')('pitaya:desktop-apps');
// Constants
var ICON_REALPATH_REGEX = /\..+$/;
+var ICON_THEMES_ROOTDIR = '/usr/share/icons';
+/**
+ * Find and load all the desktop files in the subdirectories of given dirs
+ *
+ * @param Array[String] rootDirs
+ * @return Promise
+ */
exports.loadAllDesktopFiles = function(rootDirs) {
return exports.findAllDesktopFiles(rootDirs)
@@ -28,23 +34,20 @@ exports.loadAllDesktopFiles = function(rootDirs) {
};
-exports.findAllDesktopFiles = function(rootDirs) {
+/**
+ * Find all the desktop files in the subdirectories of given dirs
+ *
+ * @param Array[String] baseDirs
+ * @return Promise
+ */
+exports.findAllDesktopFiles = function(baseDirs) {
- if(!Array.isArray(rootDirs)) {
- rootDirs = [rootDirs];
+ if(!Array.isArray(baseDirs)) {
+ baseDirs = [baseDirs];
}
- 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);
- });
- });
-
+ var promises = baseDirs.map(function(baseDir) {
+ return System.findFiles('**/*.desktop', {cwd: baseDir, realpath: true});
});
return Promise.all(promises)
@@ -55,30 +58,191 @@ exports.findAllDesktopFiles = function(rootDirs) {
};
+/**
+ * Load a .desktop file ans return its parsed content
+ *
+ * @param string filePath
+ * @return Promise
+ */
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);
- }
- });
- });
+ return System.loadINIFile(filePath);
};
-exports.findIcon = function(iconPath) {
- return new Promise(function(resolve, reject) {
- if( ICON_REALPATH_REGEX.test(iconPath) ) {
- return resolve(iconPath);
+/**
+ * Find the absolute path of a desktop icon
+ *
+ * @param string iconPath
+ * @return Promise
+ */
+exports.findIcon = function(iconName, themeName, size, themeIgnore) {
+
+ themeIgnore = themeIgnore || [];
+ if(themeIgnore.indexOf(themeIgnore) !== -1) {
+ return Promise.resolve(null);
+ }
+ themeIgnore.push(themeName);
+
+ debug('Finding icon %s:%s:%s...', iconName, themeName, size);
+
+ if( ICON_REALPATH_REGEX.test(iconName) ) {
+ return Promise.resolve(iconName);
+ }
+
+ if(!themeName) {
+ return exports.findIconThemes()
+ .then(function(themes) {
+ themeIgnore = themeIgnore || [];
+ var promises = themes.map(function(theme) {
+ return exports.findIcon(iconName, theme, size, themeIgnore);
+ });
+ return Promise.all(promises)
+ .then(exports._selectBestIcon)
+ ;
+ })
+ ;
+ }
+
+ return exports.findClosestSizeIcon(iconName, themeName, size)
+ .then(function(foundIcon) {
+
+ if(foundIcon) return foundIcon;
+
+ debug('No icon found. Search in parents...');
+
+ return exports.findParentsThemeIcon(iconName, themeName, size, themeIgnore);
+
+ })
+ ;
+
+};
+
+exports.findParentsThemeIcon = function(iconName, themeName, size, themeIgnore) {
+
+ return exports.themeIndexExists(themeName)
+ .then(function(exists) {
+
+ if(!exists) return null;
+
+ return exports.loadThemeIndex(themeName)
+ .then(function(themeIndex) {
+
+ if(!themeIndex || !themeIndex['Icon Theme'].Inherits) return;
+
+ var parents = themeIndex['Icon Theme'].Inherits.split(',');
+
+ debug('Found parents %j', parents);
+
+ var promises = parents.map(function(themeName) {
+ return exports.findIcon(iconName, themeName, size, themeIgnore);
+ });
+
+ return Promise.all(promises)
+ .then(exports._selectBestIcon)
+ ;
+
+ })
+ ;
+
+ })
+ ;
+
+};
+
+exports.findClosestSizeIcon = function(iconName, themeName, size) {
+
+ var themePath = path.join(ICON_THEMES_ROOTDIR, themeName);
+
+ var extPattern = '{svg,png}';
+ var filePattern = themeName+'/*/*/'+iconName+'.'+extPattern;
+
+ debug('File pattern %s', filePattern);
+
+ return System.findFiles(filePattern, {cwd: ICON_THEMES_ROOTDIR})
+ .then(function(iconFiles) {
+
+ debug('Found files %j', iconFiles);
+
+ var scalableIcon = iconFiles.reduce(function(scalableIcon, iconPath) {
+ if(iconPath.indexOf('scalable') !== -1) {
+ debug('Found scalable icon %s', iconPath);
+ scalableIcon = iconPath;
+ }
+ return scalableIcon;
+ }, null);
+
+ if(scalableIcon) return scalableIcon;
+
+ if(!size) {
+ size = Math.max.apply(Math, clean(iconFiles.map(sizeFromPath)));
+ }
+
+ var closestIcon = iconFiles.reduce(function(foundIcon, iconPath) {
+ var foundSize = sizeFromPath(iconPath);
+ if( foundSize && Math.abs(foundSize - size) < Math.abs(foundIcon.size - size) ) {
+ foundIcon.path = iconPath;
+ foundIcon.size = foundSize;
+ }
+ return foundIcon;
+ }, {path: null, size: null});
+
+ return closestIcon.path;
+
+ })
+ .then(function(iconPath) {
+ debug('Closest icon %j', iconPath);
+ return iconPath ? path.join(ICON_THEMES_ROOTDIR, iconPath) : null;
+ })
+ ;
+
+ function sizeFromPath(iconPath) {
+ var simpleSizeRegex = /\/(\d+)\//;
+ var matches = simpleSizeRegex.exec(iconPath);
+ if(matches && matches[1]) return +matches[1];
+ var doubleSizeRegex = /\/(\d+)x\d+\//;
+ matches = doubleSizeRegex.exec(iconPath);
+ if(matches && matches[1]) return +matches[1];
+ }
+
+};
+
+exports._selectBestIcon = function(iconPaths) {
+ var iconSelection = iconPaths.reduce(function(iconSelection, iconPath) {
+ if(iconPath) {
+ var key = iconPath.indexOf('scalable') !== -1 ? 'scalable' : 'bitmap';
+ iconSelection[key] = iconPath;
}
- });
+ return iconSelection;
+ }, {scalable: null, bitmap: null});
+ debug('Icon selection %j', iconSelection);
+ return iconSelection.scalable || iconSelection.bitmap;
+}
+
+exports.findIconThemes = function() {
+ return System.findFiles('*/', {cwd: ICON_THEMES_ROOTDIR, realpath: true})
+ .then(function(files) {
+ return files.map(path.basename.bind(path));
+ })
+ ;
+};
+
+exports.loadThemeIndex = function(themeName) {
+ var themeIndexPath = path.join(ICON_THEMES_ROOTDIR, themeName, 'index.theme');
+ return System.loadINIFile(themeIndexPath);
+};
+
+exports.themeIndexExists = function(themeName) {
+ var themeIndexPath = path.join(ICON_THEMES_ROOTDIR, themeName, 'index.theme');
+ return System.exists(themeIndexPath);
};
// Array helpers
+function clean(arr) {
+ return arr.filter(function(item) {
+ return item !== null && item !== undefined;
+ });
+}
+
function flatten(arr) {
return arr.reduce(function(result, item) {
result = result.concat.apply(result, Array.isArray(item) ? flatten(item) : [item]);
diff --git a/js/util/system.js b/js/util/system.js
index 6932710..8e14ad5 100644
--- a/js/util/system.js
+++ b/js/util/system.js
@@ -1,5 +1,7 @@
var fs = require('fs');
var cp = require('child_process');
+var glob = require('glob');
+var ini = require('ini');
/**
* Load a JSON file
@@ -21,6 +23,26 @@ exports.loadJSONFile = function(filePath) {
});
};
+/**
+ * Load a INI file
+ *
+ * @param filePath The path of the json file
+ * @return Promise
+ */
+exports.loadINIFile = 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.runApp = function(execPath) {
return new Promise(function(resolve, reject) {
cp.exec(execPath, function(err) {
@@ -29,3 +51,34 @@ exports.runApp = function(execPath) {
});
});
};
+
+
+var _globCache = {
+ statCache: {},
+ cache: {},
+ realpathCache: {},
+ symlinks: {}
+};
+
+exports.findFiles = function(pattern, opts) {
+ return new Promise(function(resolve, reject) {
+
+ opts = opts || {};
+ opts.cache = _globCache.cache;
+ opts.statCache = _globCache.statCache;
+ opts.realpathCache = _globCache.realpathCache;
+ opts.symlinks = _globCache.symlinks;
+
+ glob(pattern, opts, function(err, files) {
+ if(err) return reject(err);
+ return resolve(files);
+ });
+
+ });
+};
+
+exports.exists = function(filePath) {
+ return new Promise(function(resolve) {
+ fs.exists(filePath, resolve);
+ });
+};
diff --git a/package.json b/package.json
index 0728334..8c45c6c 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,11 @@
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-nw": "git+https://github.com/snap-project/grunt-nw#develop",
- "lodash": "^3.0.1"
+ "lodash": "^3.0.1",
+ "nodeunit": "^0.9.1"
},
"scripts": {
+ "test": "./node_modules/.bin/nodeunit test",
"start": "./node_modules/.bin/grunt pitaya:run",
"build": "./node_modules/.bin/grunt pitaya:build"
},
@@ -21,6 +23,7 @@
"kiosk": false
},
"dependencies": {
+ "debug": "^2.2.0",
"glob": "^5.0.14",
"ini": "^1.3.4",
"minimist": "^1.1.3",
diff --git a/test/desktop.js b/test/desktop.js
new file mode 100644
index 0000000..304f70f
--- /dev/null
+++ b/test/desktop.js
@@ -0,0 +1,35 @@
+var DesktopApps = require('../js/util/desktop-apps');
+
+var DesktopSuite = exports.DesktopSuite = {};
+
+
+DesktopSuite.findIconThemes = function(test) {
+
+ DesktopApps.findIconThemes()
+ .then(function(themes) {
+ //console.log(themes);
+ test.ok(themes.length > 0);
+ test.done();
+ })
+ .catch(function(err) {
+ test.ifError(err);
+ test.done();
+ })
+ ;
+
+};
+
+DesktopSuite.findIcon = function(test) {
+
+ DesktopApps.findIcon('nm-device-wireless')
+ .then(function(iconPath) {
+ //console.log('findIcon', iconPath);
+ test.done();
+ })
+ .catch(function(err) {
+ test.ifError(err);
+ test.done();
+ })
+ ;
+
+};