Base chargement des icons des applications

This commit is contained in:
2015-08-30 21:29:19 +02:00
parent f61ac744a9
commit bd5d41aa88
10 changed files with 393 additions and 52 deletions

View File

@ -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 (
<AppItem key={path} itemPath={path} item={item} onItemClick={this.props.onItemClick} />
);
}.bind(this));
// Create the apps list
return (
<ul key={parentPath} className="apps-list">
{items}

View File

@ -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 (
<li className="desktop-app">
<img src={icon} className="desktop-app-icon" />
<img src={this.state.icon} className="desktop-app-icon" />
<span className="desktop-app-label">{label}</span>
</li>
);
@ -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 });
})
;
}

View File

@ -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 });

View File

@ -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);
};
}

View File

@ -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]);

View File

@ -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);
});
};