Browse Source

Base chargement des icons des applications

feature/profil_enr
William Petit 4 years ago
parent
commit
bd5d41aa88

+ 12
- 3
css/style.css View File

@@ -48,7 +48,6 @@ html, body {
48 48
 }
49 49
 
50 50
 .launcher ul.apps-list {
51
-  display: block;
52 51
   margin: 0;
53 52
   padding: 0;
54 53
   display: flex;
@@ -105,17 +104,27 @@ html, body {
105 104
 
106 105
 /* Edit View */
107 106
 
107
+.edit {
108
+  display: flex;
109
+  width: 100%;
110
+  height: 100%;
111
+  flex-direction: column;
112
+  align-items: flex-start;
113
+}
114
+
108 115
 .edit ul.desktop-apps {
109 116
   list-style: none;
110 117
   padding: 0;
118
+  overflow-y: auto;
111 119
 }
112 120
 
113 121
 .edit li.desktop-app {
114
-  height: 30px;
122
+
115 123
 }
116 124
 
117 125
 .edit img.desktop-app-icon {
118
-  height: 30px;
126
+  height: 50px;
127
+  width: 50px;
119 128
   display: inline-block;
120 129
   vertical-align: middle;
121 130
   margin-right: 10px;

+ 1
- 0
img/hourglass.svg View File

@@ -0,0 +1 @@
1
+<?xml version="1.0" encoding="utf-8"?><svg width='50px' height='50px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-hourglass"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><g><path fill="none" stroke="#ffffff" stroke-width="5" stroke-miterlimit="10" d="M58.4,51.7c-0.9-0.9-1.4-2-1.4-2.3s0.5-0.4,1.4-1.4 C70.8,43.8,79.8,30.5,80,15.5H70H30H20c0.2,15,9.2,28.1,21.6,32.3c0.9,0.9,1.4,1.2,1.4,1.5s-0.5,1.6-1.4,2.5 C29.2,56.1,20.2,69.5,20,85.5h10h40h10C79.8,69.5,70.8,55.9,58.4,51.7z" class="glass"></path><clipPath id="uil-hourglass-clip1"><rect x="15" y="20" width="70" height="25" class="clip"><animate attributeName="height" from="25" to="0" dur="1s" repeatCount="indefinite" vlaues="25;0;0" keyTimes="0;0.5;1"></animate><animate attributeName="y" from="20" to="45" dur="1s" repeatCount="indefinite" vlaues="20;45;45" keyTimes="0;0.5;1"></animate></rect></clipPath><clipPath id="uil-hourglass-clip2"><rect x="15" y="55" width="70" height="25" class="clip"><animate attributeName="height" from="0" to="25" dur="1s" repeatCount="indefinite" vlaues="0;25;25" keyTimes="0;0.5;1"></animate><animate attributeName="y" from="80" to="55" dur="1s" repeatCount="indefinite" vlaues="80;55;55" keyTimes="0;0.5;1"></animate></rect></clipPath><path d="M29,23c3.1,11.4,11.3,19.5,21,19.5S67.9,34.4,71,23H29z" clip-path="url(#uil-hourglass-clip1)" fill="#d2d2d2" class="sand"></path><path d="M71.6,78c-3-11.6-11.5-20-21.5-20s-18.5,8.4-21.5,20H71.6z" clip-path="url(#uil-hourglass-clip2)" fill="#d2d2d2" class="sand"></path><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="180 50 50" repeatCount="indefinite" dur="1s" values="0 50 50;0 50 50;180 50 50" keyTimes="0;0.7;1"></animateTransform></g></svg>

+ 14
- 0
js/components/app-list.jsx View File

@@ -4,24 +4,38 @@ var AppItem = require('./app-item.jsx');
4 4
 module.exports = React.createClass({
5 5
 
6 6
   propTypes: {
7
+
8
+    // The app items to display in the list
7 9
     items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
10
+
11
+    // the parent item path
8 12
     parentPath: React.PropTypes.oneOfType([
9 13
       React.PropTypes.string,
10 14
       React.PropTypes.arrayOf(React.PropTypes.number)
11 15
     ]).isRequired,
16
+
17
+    // Item click handler
12 18
     onItemClick: React.PropTypes.func.isRequired,
19
+
13 20
   },
14 21
 
15 22
   render: function() {
16 23
 
17 24
     var parentPath = this.props.parentPath;
25
+
26
+    // For each items, we create an AppItem component
18 27
     var items = (this.props.items).map(function(item, i) {
28
+
29
+      // The item path identifier
19 30
       var path = parentPath+'.'+i;
31
+
20 32
       return (
21 33
         <AppItem key={path} itemPath={path} item={item} onItemClick={this.props.onItemClick} />
22 34
       );
35
+
23 36
     }.bind(this));
24 37
 
38
+    // Create the apps list
25 39
     return (
26 40
       <ul key={parentPath} className="apps-list">
27 41
         {items}

+ 17
- 15
js/components/desktop-app-item.jsx View File

@@ -1,10 +1,21 @@
1 1
 var React = require('react');
2 2
 var Util = require('../util');
3
+var LazyLoad = require('./mixins/lazy-load');
3 4
 
4 5
 module.exports = React.createClass({
5 6
 
7
+  mixins: [LazyLoad],
8
+
6 9
   getInitialState: function() {
7
-    return { icon: '' };
10
+    return { icon: 'img/hourglass.svg', loading: false };
11
+  },
12
+
13
+  onInViewport: function() {
14
+    if(!this.state.loading) {
15
+      this.setState({ loading: true });
16
+      var desktopEntry = this.props.desktopEntry;
17
+      this._findIcon(desktopEntry.Icon);
18
+    }
8 19
   },
9 20
 
10 21
   render: function() {
@@ -13,18 +24,9 @@ module.exports = React.createClass({
13 24
     var label = desktopEntry.Name;
14 25
     var category = desktopEntry.Categories;
15 26
 
16
-    // Search for best icon
17
-    var icon = '';
18
-
19
-    if(!this.state.icon) {
20
-      this._findIcon(desktopEntry.Icon);
21
-    } else {
22
-      icon = this.state.icon;
23
-    }
24
-
25 27
     return (
26 28
       <li className="desktop-app">
27
-        <img src={icon} className="desktop-app-icon" />
29
+        <img src={this.state.icon} className="desktop-app-icon" />
28 30
         <span className="desktop-app-label">{label}</span>
29 31
       </li>
30 32
     );
@@ -33,12 +35,12 @@ module.exports = React.createClass({
33 35
 
34 36
   _findIcon: function(iconPath) {
35 37
 
36
-    var desktopEntry = this.props.desktopEntry;
38
+    var self = this;
37 39
 
38
-    Util.DesktopApps.findIcon(iconPath)
40
+    Util.DesktopApps.findIcon(iconPath || 'application-default-icon')
39 41
       .then(function(iconPath) {
40
-        this.setState({ icon: iconPath });
41
-      }.bind(this))
42
+        self.setState({ icon: iconPath });
43
+      })
42 44
     ;
43 45
 
44 46
   }

+ 5
- 0
js/components/desktop-app-list.jsx View File

@@ -1,6 +1,7 @@
1 1
 var React = require('react');
2 2
 var Util = require('../util');
3 3
 var DesktopAppItem = require('./desktop-app-item.jsx');
4
+var path = require('path');
4 5
 
5 6
 module.exports = React.createClass({
6 7
 
@@ -12,6 +13,10 @@ module.exports = React.createClass({
12 13
 
13 14
   componentDidMount: function() {
14 15
     // Load system desktop apps
16
+    var baseDirs = global.process.env.XDG_DATA_DIRS.split(':').map(function(baseDir){
17
+      return path.join(baseDir, 'applications');
18
+    });
19
+
15 20
     Util.DesktopApps.loadAllDesktopFiles('/usr/share/applications')
16 21
       .then(function(desktopFiles) {
17 22
         this.setState({ desktopFiles: desktopFiles });

+ 55
- 0
js/components/mixins/lazy-load.js View File

@@ -0,0 +1,55 @@
1
+var React = require('react');
2
+
3
+module.exports = {
4
+
5
+  isInViewport: function() {
6
+
7
+    var el = React.findDOMNode(this);
8
+
9
+    if(!el) return false;
10
+
11
+    var rect = el.getBoundingClientRect();
12
+
13
+    return (
14
+      rect.top >= 0 &&
15
+      rect.left >= 0 &&
16
+      rect.bottom <= (global.window.innerHeight || global.document.documentElement.clientHeight) && /*or $(window).height() */
17
+      rect.right <= (global.window.innerWidth || global.document.documentElement.clientWidth) /*or $(window).width() */
18
+    );
19
+
20
+  },
21
+
22
+  componentDidMount: function() {
23
+
24
+    function _onInViewport(){
25
+      if( this.isInViewport() ) {
26
+        this.onInViewport();
27
+      }
28
+    }
29
+
30
+    var el = React.findDOMNode(this);
31
+
32
+    if(typeof this.onInViewport === 'function') {
33
+      el.parentNode.addEventListener('scroll', debounce(_onInViewport.bind(this), 250));
34
+    }
35
+
36
+    _onInViewport.call(this);
37
+
38
+  }
39
+
40
+};
41
+
42
+function debounce(func, wait, immediate) {
43
+  var timeout;
44
+	return function() {
45
+		var context = this, args = arguments;
46
+		var later = function() {
47
+			timeout = null;
48
+			if (!immediate) func.apply(context, args);
49
+		};
50
+		var callNow = immediate && !timeout;
51
+		clearTimeout(timeout);
52
+		timeout = setTimeout(later, wait);
53
+		if (callNow) func.apply(context, args);
54
+	};
55
+}

+ 196
- 32
js/util/desktop-apps.js View File

@@ -1,11 +1,17 @@
1
-var ini = require('ini');
2
-var glob = require('glob');
3 1
 var path = require('path');
4
-var fs = require('fs');
2
+var System = require('./system');
3
+var debug = require('debug')('pitaya:desktop-apps');
5 4
 
6 5
 // Constants
7 6
 var ICON_REALPATH_REGEX = /\..+$/;
7
+var ICON_THEMES_ROOTDIR = '/usr/share/icons';
8 8
 
9
+/**
10
+ * Find and load all the desktop files in the subdirectories of given dirs
11
+ *
12
+ * @param Array[String] rootDirs
13
+ * @return Promise
14
+ */
9 15
 exports.loadAllDesktopFiles = function(rootDirs) {
10 16
 
11 17
   return exports.findAllDesktopFiles(rootDirs)
@@ -28,23 +34,20 @@ exports.loadAllDesktopFiles = function(rootDirs) {
28 34
 
29 35
 };
30 36
 
31
-exports.findAllDesktopFiles = function(rootDirs) {
37
+/**
38
+ * Find all the desktop files in the subdirectories of given dirs
39
+ *
40
+ * @param Array[String] baseDirs
41
+ * @return Promise
42
+ */
43
+exports.findAllDesktopFiles = function(baseDirs) {
32 44
 
33
-  if(!Array.isArray(rootDirs)) {
34
-    rootDirs = [rootDirs];
45
+  if(!Array.isArray(baseDirs)) {
46
+    baseDirs = [baseDirs];
35 47
   }
36 48
 
37
-  var promises = rootDirs.map(function(rootDir) {
38
-
39
-    var globPath = path.join(rootDir, '**/*.desktop');
40
-
41
-    return new Promise(function(resolve, reject) {
42
-      glob(globPath, function(err, files) {
43
-        if(err) return reject(err);
44
-        return resolve(files);
45
-      });
46
-    });
47
-
49
+  var promises = baseDirs.map(function(baseDir) {
50
+    return System.findFiles('**/*.desktop', {cwd: baseDir, realpath: true});
48 51
   });
49 52
 
50 53
   return Promise.all(promises)
@@ -55,30 +58,191 @@ exports.findAllDesktopFiles = function(rootDirs) {
55 58
 
56 59
 };
57 60
 
61
+/**
62
+ * Load a .desktop file ans return its parsed content
63
+ *
64
+ * @param string filePath
65
+ * @return Promise
66
+ */
58 67
 exports.loadDesktopFile = function(filePath) {
59
-  return new Promise(function(resolve, reject) {
60
-    fs.readFile(filePath, 'utf8', function(err, content) {
61
-      if(err) return reject(err);
62
-      try {
63
-        var decoded = ini.decode(content);
64
-        return resolve(decoded);
65
-      } catch(err) {
66
-        return reject(err);
68
+  return System.loadINIFile(filePath);
69
+};
70
+
71
+/**
72
+ * Find the absolute path of a desktop icon
73
+ *
74
+ * @param string iconPath
75
+ * @return Promise
76
+ */
77
+exports.findIcon = function(iconName, themeName, size, themeIgnore) {
78
+
79
+  themeIgnore = themeIgnore || [];
80
+  if(themeIgnore.indexOf(themeIgnore) !== -1) {
81
+    return Promise.resolve(null);
82
+  }
83
+  themeIgnore.push(themeName);
84
+
85
+  debug('Finding icon %s:%s:%s...', iconName, themeName, size);
86
+
87
+  if( ICON_REALPATH_REGEX.test(iconName) ) {
88
+    return Promise.resolve(iconName);
89
+  }
90
+
91
+  if(!themeName) {
92
+    return exports.findIconThemes()
93
+      .then(function(themes) {
94
+        themeIgnore = themeIgnore || [];
95
+        var promises = themes.map(function(theme) {
96
+          return exports.findIcon(iconName, theme, size, themeIgnore);
97
+        });
98
+        return Promise.all(promises)
99
+          .then(exports._selectBestIcon)
100
+        ;
101
+      })
102
+    ;
103
+  }
104
+
105
+  return exports.findClosestSizeIcon(iconName, themeName, size)
106
+    .then(function(foundIcon) {
107
+
108
+      if(foundIcon) return foundIcon;
109
+
110
+      debug('No icon found. Search in parents...');
111
+
112
+      return exports.findParentsThemeIcon(iconName, themeName, size, themeIgnore);
113
+
114
+    })
115
+  ;
116
+
117
+};
118
+
119
+exports.findParentsThemeIcon = function(iconName, themeName, size, themeIgnore) {
120
+
121
+  return exports.themeIndexExists(themeName)
122
+    .then(function(exists) {
123
+
124
+      if(!exists) return null;
125
+
126
+      return exports.loadThemeIndex(themeName)
127
+        .then(function(themeIndex) {
128
+
129
+          if(!themeIndex || !themeIndex['Icon Theme'].Inherits) return;
130
+
131
+          var parents = themeIndex['Icon Theme'].Inherits.split(',');
132
+
133
+          debug('Found parents %j', parents);
134
+
135
+          var promises = parents.map(function(themeName) {
136
+            return exports.findIcon(iconName, themeName, size, themeIgnore);
137
+          });
138
+
139
+          return Promise.all(promises)
140
+            .then(exports._selectBestIcon)
141
+          ;
142
+
143
+        })
144
+      ;
145
+
146
+    })
147
+  ;
148
+
149
+};
150
+
151
+exports.findClosestSizeIcon = function(iconName, themeName, size) {
152
+
153
+  var themePath = path.join(ICON_THEMES_ROOTDIR, themeName);
154
+
155
+  var extPattern = '{svg,png}';
156
+  var filePattern = themeName+'/*/*/'+iconName+'.'+extPattern;
157
+
158
+  debug('File pattern %s', filePattern);
159
+
160
+  return System.findFiles(filePattern, {cwd: ICON_THEMES_ROOTDIR})
161
+    .then(function(iconFiles) {
162
+
163
+      debug('Found files %j', iconFiles);
164
+
165
+      var scalableIcon = iconFiles.reduce(function(scalableIcon, iconPath) {
166
+        if(iconPath.indexOf('scalable') !== -1) {
167
+          debug('Found scalable icon %s', iconPath);
168
+          scalableIcon = iconPath;
169
+        }
170
+        return scalableIcon;
171
+      }, null);
172
+
173
+      if(scalableIcon) return scalableIcon;
174
+
175
+      if(!size) {
176
+        size = Math.max.apply(Math, clean(iconFiles.map(sizeFromPath)));
67 177
       }
68
-    });
69
-  });
178
+
179
+      var closestIcon = iconFiles.reduce(function(foundIcon, iconPath) {
180
+        var foundSize = sizeFromPath(iconPath);
181
+        if( foundSize && Math.abs(foundSize - size) < Math.abs(foundIcon.size - size) ) {
182
+          foundIcon.path = iconPath;
183
+          foundIcon.size = foundSize;
184
+        }
185
+        return foundIcon;
186
+      }, {path: null, size: null});
187
+
188
+      return closestIcon.path;
189
+
190
+    })
191
+    .then(function(iconPath) {
192
+      debug('Closest icon %j', iconPath);
193
+      return iconPath ? path.join(ICON_THEMES_ROOTDIR, iconPath) : null;
194
+    })
195
+  ;
196
+
197
+  function sizeFromPath(iconPath) {
198
+    var simpleSizeRegex = /\/(\d+)\//;
199
+    var matches = simpleSizeRegex.exec(iconPath);
200
+    if(matches && matches[1]) return +matches[1];
201
+    var doubleSizeRegex = /\/(\d+)x\d+\//;
202
+    matches = doubleSizeRegex.exec(iconPath);
203
+    if(matches && matches[1]) return +matches[1];
204
+  }
205
+
70 206
 };
71 207
 
72
-exports.findIcon = function(iconPath) {
73
-  return new Promise(function(resolve, reject) {
74
-    if( ICON_REALPATH_REGEX.test(iconPath) ) {
75
-      return resolve(iconPath);
208
+exports._selectBestIcon = function(iconPaths) {
209
+  var iconSelection = iconPaths.reduce(function(iconSelection, iconPath) {
210
+    if(iconPath) {
211
+      var key = iconPath.indexOf('scalable') !== -1 ? 'scalable' : 'bitmap';
212
+      iconSelection[key] = iconPath;
76 213
     }
77
-  });
214
+    return iconSelection;
215
+  }, {scalable: null, bitmap: null});
216
+  debug('Icon selection %j', iconSelection);
217
+  return iconSelection.scalable || iconSelection.bitmap;
218
+}
219
+
220
+exports.findIconThemes = function() {
221
+  return System.findFiles('*/', {cwd: ICON_THEMES_ROOTDIR, realpath: true})
222
+    .then(function(files) {
223
+      return files.map(path.basename.bind(path));
224
+    })
225
+  ;
226
+};
227
+
228
+exports.loadThemeIndex = function(themeName) {
229
+  var themeIndexPath = path.join(ICON_THEMES_ROOTDIR, themeName, 'index.theme');
230
+  return System.loadINIFile(themeIndexPath);
231
+};
232
+
233
+exports.themeIndexExists = function(themeName) {
234
+  var themeIndexPath = path.join(ICON_THEMES_ROOTDIR, themeName, 'index.theme');
235
+  return System.exists(themeIndexPath);
78 236
 };
79 237
 
80 238
 // Array helpers
81 239
 
240
+function clean(arr) {
241
+  return arr.filter(function(item) {
242
+    return item !== null && item !== undefined;
243
+  });
244
+}
245
+
82 246
 function flatten(arr) {
83 247
   return arr.reduce(function(result, item) {
84 248
     result = result.concat.apply(result, Array.isArray(item) ? flatten(item) : [item]);

+ 53
- 0
js/util/system.js View File

@@ -1,5 +1,7 @@
1 1
 var fs = require('fs');
2 2
 var cp = require('child_process');
3
+var glob = require('glob');
4
+var ini = require('ini');
3 5
 
4 6
 /**
5 7
  * Load a JSON file
@@ -21,6 +23,26 @@ exports.loadJSONFile = function(filePath) {
21 23
   });
22 24
 };
23 25
 
26
+/**
27
+ * Load a INI file
28
+ *
29
+ * @param filePath The path of the json file
30
+ * @return Promise
31
+ */
32
+exports.loadINIFile = function(filePath) {
33
+  return new Promise(function(resolve, reject) {
34
+    fs.readFile(filePath, 'utf8', function(err, content) {
35
+      if(err) return reject(err);
36
+      try {
37
+        var decoded = ini.decode(content);
38
+        return resolve(decoded);
39
+      } catch(err) {
40
+        return reject(err);
41
+      }
42
+    });
43
+  });
44
+};
45
+
24 46
 exports.runApp = function(execPath) {
25 47
   return new Promise(function(resolve, reject) {
26 48
     cp.exec(execPath, function(err) {
@@ -29,3 +51,34 @@ exports.runApp = function(execPath) {
29 51
     });
30 52
   });
31 53
 };
54
+
55
+
56
+var _globCache = {
57
+  statCache: {},
58
+  cache: {},
59
+  realpathCache: {},
60
+  symlinks: {}
61
+};
62
+
63
+exports.findFiles = function(pattern, opts) {
64
+  return new Promise(function(resolve, reject) {
65
+
66
+    opts = opts || {};
67
+    opts.cache = _globCache.cache;
68
+    opts.statCache = _globCache.statCache;
69
+    opts.realpathCache = _globCache.realpathCache;
70
+    opts.symlinks = _globCache.symlinks;
71
+
72
+    glob(pattern, opts, function(err, files) {
73
+      if(err) return reject(err);
74
+      return resolve(files);
75
+    });
76
+
77
+  });
78
+};
79
+
80
+exports.exists = function(filePath) {
81
+  return new Promise(function(resolve) {
82
+    fs.exists(filePath, resolve);
83
+  });
84
+};

+ 4
- 1
package.json View File

@@ -9,9 +9,11 @@
9 9
     "grunt-contrib-clean": "^0.6.0",
10 10
     "grunt-contrib-copy": "^0.7.0",
11 11
     "grunt-nw": "git+https://github.com/snap-project/grunt-nw#develop",
12
-    "lodash": "^3.0.1"
12
+    "lodash": "^3.0.1",
13
+    "nodeunit": "^0.9.1"
13 14
   },
14 15
   "scripts": {
16
+    "test": "./node_modules/.bin/nodeunit test",
15 17
     "start": "./node_modules/.bin/grunt pitaya:run",
16 18
     "build": "./node_modules/.bin/grunt pitaya:build"
17 19
   },
@@ -21,6 +23,7 @@
21 23
     "kiosk": false
22 24
   },
23 25
   "dependencies": {
26
+    "debug": "^2.2.0",
24 27
     "glob": "^5.0.14",
25 28
     "ini": "^1.3.4",
26 29
     "minimist": "^1.1.3",

+ 35
- 0
test/desktop.js View File

@@ -0,0 +1,35 @@
1
+var DesktopApps = require('../js/util/desktop-apps');
2
+
3
+var DesktopSuite = exports.DesktopSuite = {};
4
+
5
+
6
+DesktopSuite.findIconThemes = function(test) {
7
+
8
+  DesktopApps.findIconThemes()
9
+    .then(function(themes) {
10
+      //console.log(themes);
11
+      test.ok(themes.length > 0);
12
+      test.done();
13
+    })
14
+    .catch(function(err) {
15
+      test.ifError(err);
16
+      test.done();
17
+    })
18
+  ;
19
+
20
+};
21
+
22
+DesktopSuite.findIcon = function(test) {
23
+
24
+  DesktopApps.findIcon('nm-device-wireless')
25
+    .then(function(iconPath) {
26
+      //console.log('findIcon', iconPath);
27
+      test.done();
28
+    })
29
+    .catch(function(err) {
30
+      test.ifError(err);
31
+      test.done();
32
+    })
33
+  ;
34
+
35
+};

Loading…
Cancel
Save