Ajout police Sawaasdee, rename js to src
This commit is contained in:
43
src/app.js
Normal file
43
src/app.js
Normal file
@ -0,0 +1,43 @@
|
||||
var React = require('react');
|
||||
var LauncherView = require('./components/launcher/launcher-view.js');
|
||||
var EditView = require('./components/edit/edit-view.js');
|
||||
var Provider = require('react-redux').Provider;
|
||||
var connect = require('react-redux').connect;
|
||||
var store = require('./store');
|
||||
|
||||
// Main component
|
||||
var App = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
var editMode = process.env.PITAYA_MODE === 'edit';
|
||||
|
||||
var view = editMode ? <EditView /> : <LauncherView />;
|
||||
|
||||
return (
|
||||
<div id="pitaya">
|
||||
{view}
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// Select props to inject from store state
|
||||
function select(state) {
|
||||
return {
|
||||
processOpts: state.processOpts
|
||||
}
|
||||
}
|
||||
|
||||
// Connect App to Redux store
|
||||
App = connect(select)(App);
|
||||
|
||||
React.render(
|
||||
<Provider store={store}>
|
||||
{ function() { return <App />; } }
|
||||
</Provider>
|
||||
,
|
||||
document.body
|
||||
);
|
81
src/components/common/app-icon.js
Normal file
81
src/components/common/app-icon.js
Normal file
@ -0,0 +1,81 @@
|
||||
var React = require('react');
|
||||
var Util = require('../../util');
|
||||
var LazyLoad = require('../mixins/lazy-load');
|
||||
var debug = Util.Debug('common:app-icon');
|
||||
|
||||
var LOADING_ICON = 'img/hourglass.svg';
|
||||
var DEFAULT_ICON = 'img/default-icon.svg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
mixins: [LazyLoad],
|
||||
|
||||
getInitialState: function() {
|
||||
return { icon: DEFAULT_ICON, currentTheme: '' };
|
||||
},
|
||||
|
||||
onInViewport: function() {
|
||||
this.updateIconIfInViewport();
|
||||
},
|
||||
|
||||
updateIconIfInViewport: function() {
|
||||
|
||||
var currentTheme = this.state.currentTheme;
|
||||
var newTheme = this.props.theme;
|
||||
|
||||
if( !this.isInViewport() || newTheme === currentTheme ) return;
|
||||
|
||||
this.setState({ icon: LOADING_ICON, currentTheme: newTheme });
|
||||
|
||||
var desktopEntry = this.props.desktopEntry;
|
||||
|
||||
this._findIcon(this.props.icon, newTheme);
|
||||
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this.updateIconIfInViewport();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var icon = this.state.icon;
|
||||
|
||||
var style = {
|
||||
backgroundImage: 'url('+icon+')'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-icon" style={style}></div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
_findIcon: function(iconPath, theme) {
|
||||
|
||||
var self = this;
|
||||
|
||||
debug('Search icon %s:%s', iconPath, theme);
|
||||
|
||||
Util.DesktopApps.findIcon(iconPath || DEFAULT_ICON, theme)
|
||||
.then(function(iconPath) {
|
||||
if( !iconPath || /\.xpm$/.test(iconPath) ) {
|
||||
return Util.DesktopApps.findIcon(DEFAULT_ICON, theme);
|
||||
}
|
||||
return iconPath;
|
||||
})
|
||||
.then(function(iconPath) {
|
||||
return Util.System.exists(iconPath)
|
||||
.then(function(exists) {
|
||||
return exists ? iconPath : DEFAULT_ICON;
|
||||
})
|
||||
;
|
||||
})
|
||||
.then(function(iconPath) {
|
||||
self.setState({ icon: iconPath });
|
||||
})
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
});
|
55
src/components/edit/desktop-app-item.js
Normal file
55
src/components/edit/desktop-app-item.js
Normal file
@ -0,0 +1,55 @@
|
||||
var React = require('react');
|
||||
var Util = require('../../util');
|
||||
var AppIcon = require('../common/app-icon.js');
|
||||
var DragSource = require('react-dnd').DragSource;
|
||||
|
||||
var DesktopAppItem = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
var desktopEntry = this.props.desktopEntry;
|
||||
var label = desktopEntry.Name;
|
||||
var category = desktopEntry.Categories;
|
||||
var icon = desktopEntry.Icon;
|
||||
|
||||
var connectDragSource = this.props.connectDragSource;
|
||||
|
||||
return connectDragSource(
|
||||
<li className="desktop-app list-group-item">
|
||||
<AppIcon className="desktop-app-icon" icon={icon} theme={this.props.theme} />
|
||||
<span className="desktop-app-label">{label}</span>
|
||||
</li>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var dragSourceSpec = {
|
||||
|
||||
beginDrag: function(props) {
|
||||
return props;
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor) {
|
||||
|
||||
if (!monitor.didDrop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dropResult = monitor.getDropResult();
|
||||
|
||||
return props.onItemDropped(props.desktopEntry, dropResult);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function dragSourceCollect(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = DragSource('NEW_ITEM', dragSourceSpec, dragSourceCollect)(DesktopAppItem);
|
33
src/components/edit/desktop-app-list.js
Normal file
33
src/components/edit/desktop-app-list.js
Normal file
@ -0,0 +1,33 @@
|
||||
var React = require('react');
|
||||
var Util = require('../../util');
|
||||
var DesktopAppItem = require('./desktop-app-item.js');
|
||||
var path = require('path');
|
||||
var debug = require('../../util/debug')('pitaya:desktop-app-list');
|
||||
|
||||
var DesktopAppList = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
var items = this.props.desktopApps.map(function(desktopApp, i) {
|
||||
var desktopEntry = desktopApp.content['Desktop Entry'];
|
||||
return (
|
||||
<DesktopAppItem theme={this.props.theme}
|
||||
key={desktopApp.path}
|
||||
desktopEntry={desktopEntry}
|
||||
onItemDropped={this.props.onItemDropped} />
|
||||
);
|
||||
}.bind(this));
|
||||
|
||||
return (
|
||||
<div className="apps-list">
|
||||
<ul className="desktop-apps list-group">
|
||||
{items}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = DesktopAppList;
|
103
src/components/edit/edit-view.js
Normal file
103
src/components/edit/edit-view.js
Normal file
@ -0,0 +1,103 @@
|
||||
var React = require('react');
|
||||
var connect = require('react-redux').connect;
|
||||
var ProfileTree = require('./profile-tree.js');
|
||||
var DesktopAppList = require('./desktop-app-list.js');
|
||||
var ItemForm = require('./item-form.js');
|
||||
var IconThemeSelector = require('./icon-theme-selector.js');
|
||||
var ProfileMenu = require('./profile-menu.js');
|
||||
var tree = require('../../util/tree');
|
||||
|
||||
var actions = require('../../store/actions');
|
||||
var DragDropContext = require('react-dnd').DragDropContext;
|
||||
var HTML5Backend = require('react-dnd/modules/backends/HTML5');
|
||||
|
||||
var EditView = React.createClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.dispatch(actions.edit.loadDesktopApps());
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="edit">
|
||||
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<div className="menu-bar">
|
||||
<div className="left">
|
||||
<ProfileMenu />
|
||||
</div>
|
||||
<div className="main">
|
||||
<div className="full-width">
|
||||
<button className="btn btn-primary pull-right btn-sm" onClick={this.handleAddNewNode}>Ajouter un noeud</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right"></div>
|
||||
</div>
|
||||
<div className="workspace">
|
||||
<div className="left">
|
||||
<div className="apps-menu">
|
||||
<b className="title">Thème</b>
|
||||
<IconThemeSelector onThemeSelected={this.handleThemeSelect} />
|
||||
<b className="title">Applications</b>
|
||||
<DesktopAppList
|
||||
theme={this.props.theme}
|
||||
desktopApps={this.props.desktopApps}
|
||||
onItemDropped={this.handleItemDrop} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="main">
|
||||
<b className="title">Arbre de profil</b>
|
||||
<ProfileTree />
|
||||
</div>
|
||||
<div className="right">
|
||||
<b className="title">Édition</b>
|
||||
<ItemForm item={this.props.selectedItem} onItemChange={this.handleItemChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
handleItemDrop: function(desktopEntry, targetItem) {
|
||||
|
||||
var newProfileItem = {
|
||||
label: desktopEntry.Name,
|
||||
icon: desktopEntry.Icon,
|
||||
exec: desktopEntry.Exec
|
||||
};
|
||||
|
||||
this.props.dispatch(actions.edit.addProfileItem(newProfileItem, targetItem));
|
||||
|
||||
},
|
||||
|
||||
handleThemeSelect: function(theme) {
|
||||
this.props.dispatch(actions.edit.useIconTheme(theme));
|
||||
},
|
||||
|
||||
handleItemChange: function(item, key, value) {
|
||||
this.props.dispatch(actions.edit.updateProfileItem(item, key, value));
|
||||
},
|
||||
|
||||
handleAddNewNode: function() {
|
||||
var newItem = {
|
||||
label: 'Nouveau noeud',
|
||||
icon: '',
|
||||
exec: '',
|
||||
background: ''
|
||||
};
|
||||
this.props.dispatch(actions.edit.addProfileItem(newItem, this.props.profile));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
desktopApps: state.desktopApps,
|
||||
profile: state.profile,
|
||||
theme: state.theme,
|
||||
selectedItem: tree.matches(state.profile, {selected: true})[0]
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = DragDropContext(HTML5Backend)(connect(select)(EditView));
|
51
src/components/edit/icon-theme-selector.js
Normal file
51
src/components/edit/icon-theme-selector.js
Normal file
@ -0,0 +1,51 @@
|
||||
var React = require('react');
|
||||
var Util = require('../../util');
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
propsType: {
|
||||
onThemeSelected: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return { selectedTheme: null, availableThemes: [] };
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
Util.DesktopApps.findIconThemes()
|
||||
.then(function(themes) {
|
||||
this.setState({ availableThemes: themes });
|
||||
}.bind(this))
|
||||
;
|
||||
},
|
||||
|
||||
onChange: function(evt) {
|
||||
var selectedTheme = evt.target.value;
|
||||
this.setState({ selectedTheme: selectedTheme });
|
||||
this.props.onThemeSelected(selectedTheme);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var selectedTheme = this.state.selectedTheme;
|
||||
var options = this.state.availableThemes.map(function(theme) {
|
||||
return (
|
||||
<option key={theme} value={theme}>{theme}</option>
|
||||
);
|
||||
});
|
||||
|
||||
options.unshift(
|
||||
<option key="__none__"></option>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="icon-theme-selector">
|
||||
<select className="form-control" value={selectedTheme} onChange={this.onChange}>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
80
src/components/edit/item-form.js
Normal file
80
src/components/edit/item-form.js
Normal file
@ -0,0 +1,80 @@
|
||||
var React = require('react');
|
||||
|
||||
var ItemForm = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
label: '',
|
||||
icon: '',
|
||||
exec: ''
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(props) {
|
||||
|
||||
if(props.item) {
|
||||
|
||||
this.setState({
|
||||
label: props.item.label,
|
||||
icon: props.item.icon,
|
||||
exec: props.item.exec,
|
||||
background: props.item.background
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var state = this.state;
|
||||
|
||||
return (
|
||||
<div className="item-form">
|
||||
<form>
|
||||
<div className="form-group">
|
||||
<input type="text" className="form-control"
|
||||
placeholder="Label"
|
||||
value={state.label}
|
||||
onChange={this.handleChange.bind(this, 'label')} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="text" className="form-control"
|
||||
placeholder="Icône"
|
||||
value={state.icon}
|
||||
onChange={this.handleChange.bind(this, 'icon')} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="text" className="form-control"
|
||||
placeholder="Chemin d'exécution" value={state.exec}
|
||||
onChange={this.handleChange.bind(this, 'exec')} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="text" className="form-control"
|
||||
placeholder="Fond d'écran" value={state.background}
|
||||
onChange={this.handleChange.bind(this, 'background')} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
handleChange: function(key, evt) {
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
var newState = {};
|
||||
var value = evt.currentTarget.value;
|
||||
|
||||
newState[key] = value;
|
||||
this.setState(newState);
|
||||
|
||||
if(typeof this.props.onItemChange === 'function') {
|
||||
this.props.onItemChange(this.props.item, key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = ItemForm;
|
85
src/components/edit/profile-menu.js
Normal file
85
src/components/edit/profile-menu.js
Normal file
@ -0,0 +1,85 @@
|
||||
/* jhsint node:true, jsx: true */
|
||||
var React = require('react');
|
||||
var connect = require('react-redux').connect;
|
||||
var actions = require('../../store/actions');
|
||||
var dialog = require('remote').require('dialog');
|
||||
|
||||
var ProfileMenu = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="profile-menu">
|
||||
<button className="btn btn-default btn-sm" onClick={this.handleOpenClick}>Ouvrir</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={this.handleSaveClick}>Enregistrer</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
handleOpenClick: function() {
|
||||
var dispatch = this.props.dispatch;
|
||||
this.showOpenProfileDialog()
|
||||
.then(function(profilePath) {
|
||||
if(profilePath) dispatch(actions.common.loadProfile(profilePath));
|
||||
})
|
||||
;
|
||||
},
|
||||
|
||||
handleSaveClick: function() {
|
||||
|
||||
var dispatch = this.props.dispatch;
|
||||
var profile = this.props.profile;
|
||||
var profilePath = this.props.profilePath;
|
||||
|
||||
this.showSaveProfileDialog(profilePath)
|
||||
.then(function(profilePath) {
|
||||
if(profilePath) dispatch(actions.edit.saveProfile(profilePath, profile));
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showOpenProfileDialog: function() {
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
dialog.showOpenDialog(
|
||||
{
|
||||
title: 'Éditer un profil',
|
||||
filters: [ {name: 'Profils Pitaya', extensions: ['json'] } ],
|
||||
properties: ['openFile']
|
||||
},
|
||||
function(files) {
|
||||
return resolve(files ? files[0] : null);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showSaveProfileDialog: function(defaultPath) {
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
dialog.showSaveDialog(
|
||||
{
|
||||
defaultPath: defaultPath,
|
||||
title: 'Enregistrer un profil',
|
||||
filters: [ {name: 'Profils Pitaya', extensions: ['json'] } ]
|
||||
},
|
||||
function(file) {
|
||||
return resolve(file);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
profile: state.profile,
|
||||
profilePath: state.profilePath
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = connect(select)(ProfileMenu);
|
96
src/components/edit/profile-tree.js
Normal file
96
src/components/edit/profile-tree.js
Normal file
@ -0,0 +1,96 @@
|
||||
var React = require('react');
|
||||
var connect = require('react-redux').connect;
|
||||
var actions = require('../../store/actions');
|
||||
var TreeItem = require('./tree-item.js');
|
||||
|
||||
var TreeNode = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
var data = this.props.data || {};
|
||||
var subItems = data.items || [];
|
||||
|
||||
var listElements = subItems.map(function(subItem, i) {
|
||||
return (
|
||||
<li key={i} >
|
||||
<TreeNode data={subItem}
|
||||
selectedItem={this.props.selectedItem}
|
||||
onItemClicked={this.props.onItemClicked}
|
||||
onItemMoved={this.props.onItemMoved}
|
||||
onItemRemoved={this.props.onItemRemoved}
|
||||
theme={this.props.theme} />
|
||||
</li>
|
||||
);
|
||||
}.bind(this));
|
||||
|
||||
var appEntry = data.icon || data.label ?
|
||||
this.renderTreeItem(data):
|
||||
null
|
||||
;
|
||||
|
||||
return (
|
||||
<div className="tree-item">
|
||||
{appEntry}
|
||||
<ul>
|
||||
{listElements}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
renderTreeItem: function(data) {
|
||||
return (
|
||||
<TreeItem data={data}
|
||||
selected={data.selected}
|
||||
{...this.props} />
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var ProfileTree = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="profile-tree">
|
||||
{this.renderTreeNode(this.props.profile)}
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
renderTreeNode: function(data) {
|
||||
return (
|
||||
<TreeNode data={data}
|
||||
selectedItem={this.props.selectedItem}
|
||||
onItemClicked={this.onItemSelected}
|
||||
onItemMoved={this.onItemMoved}
|
||||
onItemRemoved={this.onItemRemoved}
|
||||
theme={this.props.theme} />
|
||||
);
|
||||
},
|
||||
|
||||
onItemMoved: function(movedItem, targetItem) {
|
||||
this.props.dispatch(actions.edit.moveProfileItem(movedItem, targetItem));
|
||||
},
|
||||
|
||||
onItemSelected: function(selectedItem) {
|
||||
this.props.dispatch(actions.edit.selectProfileItem(selectedItem));
|
||||
},
|
||||
|
||||
onItemRemoved: function(selectedItem) {
|
||||
this.props.dispatch(actions.edit.removeProfileItem(selectedItem));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
profile: state.profile,
|
||||
theme: state.theme
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = connect(select)(ProfileTree);
|
104
src/components/edit/tree-item.js
Normal file
104
src/components/edit/tree-item.js
Normal file
@ -0,0 +1,104 @@
|
||||
var React = require('react/addons');
|
||||
var classNames = require('classnames');
|
||||
var AppIcon = require('../common/app-icon.js');
|
||||
var DragSource = require('react-dnd').DragSource;
|
||||
var DropTarget = require('react-dnd').DropTarget;
|
||||
var _ = require('lodash');
|
||||
|
||||
var TreeItem = React.createClass({
|
||||
|
||||
render: function() {
|
||||
|
||||
var data = this.props.data;
|
||||
var appIcon = data.icon ? <AppIcon icon={data.icon} theme={this.props.theme} /> : null;
|
||||
|
||||
var connectDragSource = this.props.connectDragSource;
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
|
||||
var classes = classNames({
|
||||
'alert': true,
|
||||
'alert-default': !this.props.isOver,
|
||||
'alert-info': this.props.isOver && this.props.canDrop,
|
||||
'alert-success': this.props.selected
|
||||
});
|
||||
|
||||
var style = {
|
||||
opacity: this.props.isDragging ? 0.5 : 1
|
||||
};
|
||||
|
||||
return connectDropTarget(connectDragSource(
|
||||
<div className={classes} style={style} onClick={this.handleClick}>
|
||||
{appIcon}
|
||||
<span className="app-label">{data.label}</span>
|
||||
<button type="button" className="close pull-right" onClick={this.handleRemoveClick}>
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
));
|
||||
|
||||
},
|
||||
|
||||
handleClick: function(evt) {
|
||||
evt.preventDefault();
|
||||
this.props.onItemClicked(this.props.data);
|
||||
},
|
||||
|
||||
handleRemoveClick: function(evt) {
|
||||
evt.preventDefault();
|
||||
this.props.onItemRemoved(this.props.data);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var dragSourceSpec = {
|
||||
|
||||
beginDrag: function(props) {
|
||||
return props.data;
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor) {
|
||||
|
||||
if (!monitor.didDrop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dropResult = monitor.getDropResult();
|
||||
|
||||
return props.onItemMoved(props.data, dropResult);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var dropTargetSpec = {
|
||||
|
||||
drop: function(props, monitor, component) {
|
||||
return props.data;
|
||||
},
|
||||
|
||||
canDrop: function(props, monitor) {
|
||||
var draggedItem = monitor.getItem();
|
||||
return !_.isEqual(draggedItem, props.data);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function dragSourceCollect(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function dropTargetCollect(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module.exports = DropTarget(['ITEM', 'NEW_ITEM'], dropTargetSpec, dropTargetCollect)(
|
||||
DragSource('ITEM', dragSourceSpec, dragSourceCollect)(TreeItem)
|
||||
);
|
29
src/components/launcher/app-item.js
Normal file
29
src/components/launcher/app-item.js
Normal file
@ -0,0 +1,29 @@
|
||||
var React = require('react');
|
||||
var AppIcon = require('../common/app-icon.js');
|
||||
|
||||
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 (
|
||||
<li className="app-item" onClick={this._onItemClick}>
|
||||
<AppIcon icon={this.props.item.icon} theme={null} />
|
||||
<span className="app-label">{this.props.item.label}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
47
src/components/launcher/app-list.js
Normal file
47
src/components/launcher/app-list.js
Normal file
@ -0,0 +1,47 @@
|
||||
var React = require('react');
|
||||
var AppItem = require('./app-item.js');
|
||||
|
||||
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}
|
||||
</ul>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
19
src/components/launcher/category-header.js
Normal file
19
src/components/launcher/category-header.js
Normal file
@ -0,0 +1,19 @@
|
||||
var React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="category-header">
|
||||
<span className="category-label">{this.props.item.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
168
src/components/launcher/launcher-view.js
Normal file
168
src/components/launcher/launcher-view.js
Normal file
@ -0,0 +1,168 @@
|
||||
var React = require('react');
|
||||
var CategoryHeader = require('./category-header.js');
|
||||
var AppList = require('./app-list.js');
|
||||
var Nav = require('./nav.js');
|
||||
var AnimateMixin = require('../mixins/animate');
|
||||
var actions = require('../../store/actions');
|
||||
var connect = require('react-redux').connect;
|
||||
var debug = require('../../util/debug')('launcher-view');
|
||||
var path = require('path');
|
||||
|
||||
var DEFAULT_PROFILE = path.join(__dirname, '..', '..', '..', 'default-profile.json');
|
||||
|
||||
var LauncherView = React.createClass({
|
||||
|
||||
mixins: [AnimateMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
currentItemPath: '',
|
||||
currentItem: null
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var profilePath = process.env.PITAYA_PROFILE || DEFAULT_PROFILE;
|
||||
this.props.dispatch(actions.common.loadProfile(profilePath));
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if( nextProps.profile && !this.state.currentItem ) {
|
||||
this.setState({ currentItem: nextProps.profile });
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
var currentItem = this.state.currentItem;
|
||||
var items = currentItem && currentItem.items ? currentItem.items : [];
|
||||
var currentItemPath = this.state.currentItemPath;
|
||||
|
||||
var header = currentItemPath !== '' ?
|
||||
( <CategoryHeader
|
||||
item={currentItem} /> ) :
|
||||
null
|
||||
;
|
||||
|
||||
var nav = currentItemPath !== '' ?
|
||||
( <Nav
|
||||
onBackClick={this.onBackClick}
|
||||
item={currentItem}
|
||||
itemPath={currentItemPath} /> ) :
|
||||
null
|
||||
;
|
||||
|
||||
var style = {};
|
||||
|
||||
if(currentItem && currentItem.background) {
|
||||
style.backgroundImage = 'url('+currentItem.background+')';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="launcher" style={style}>
|
||||
{header}
|
||||
<div className="main">
|
||||
{nav}
|
||||
<AppList ref="appList"
|
||||
items={items}
|
||||
parentPath={currentItemPath}
|
||||
onItemClick={this.onItemClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
onBackClick: function(itemPath) {
|
||||
|
||||
var parentPath = this._normalizeItemPath(itemPath).slice(0, -1);
|
||||
var parentItem = this._getItemByPath(parentPath, this.props.profile);
|
||||
|
||||
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) {
|
||||
|
||||
debug('Launching application "'+item.exec+'"...');
|
||||
var el = evt.currentTarget;
|
||||
el.classList.add('pulse');
|
||||
|
||||
this.props.dispatch(actions.launcher.runApp(item.exec))
|
||||
.then(function() {
|
||||
el.classList.remove('pulse');
|
||||
})
|
||||
.catch(function() {
|
||||
el.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) {
|
||||
|
||||
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;
|
||||
}, []);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
processOpts: state.processOpts,
|
||||
profile: state.profile
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module.exports = connect(select)(LauncherView);
|
30
src/components/launcher/nav.js
Normal file
30
src/components/launcher/nav.js
Normal file
@ -0,0 +1,30 @@
|
||||
/* jshint node: true jsx: true */
|
||||
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 (
|
||||
<div className="nav">
|
||||
<a href="#" onClick={this._onBackClick} className="goback" >◄</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
_onBackClick: function(evt) {
|
||||
evt.preventDefault();
|
||||
this.props.onBackClick(this.props.itemPath, this.props.item);
|
||||
}
|
||||
|
||||
});
|
23
src/components/mixins/animate.js
Normal file
23
src/components/mixins/animate.js
Normal file
@ -0,0 +1,23 @@
|
||||
var Events = {
|
||||
ANIMATION_END: 'webkitAnimationEnd'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
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;
|
||||
|
||||
function onAnimEnd(evt) {
|
||||
el.removeEventListener(Events.ANIMATION_END, onAnimEnd);
|
||||
return resolve(el);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
};
|
76
src/components/mixins/lazy-load.js
Normal file
76
src/components/mixins/lazy-load.js
Normal file
@ -0,0 +1,76 @@
|
||||
var React = require('react');
|
||||
|
||||
var _listeners = [];
|
||||
|
||||
module.exports = {
|
||||
|
||||
isInViewport: function() {
|
||||
|
||||
var el = React.findDOMNode(this);
|
||||
|
||||
if(!el) return false;
|
||||
|
||||
var rect = el.getBoundingClientRect();
|
||||
|
||||
var viewportHeight = global.window.innerHeight || global.document.documentElement.clientHeight;
|
||||
var viewportWidth = global.window.innerWidth || global.document.documentElement.clientWidth;
|
||||
|
||||
return (
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= viewportHeight &&
|
||||
rect.left <= viewportWidth
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
||||
if(typeof this.onInViewport === 'function') {
|
||||
|
||||
_listeners.push(this);
|
||||
|
||||
if( this.isInViewport() ) {
|
||||
this.onInViewport();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var index = _listeners.indexOf(this);
|
||||
if(index !== -1) return _listeners.splice(index, 1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
var computeComponentsVisibilityDebounced = debounce(computeComponentsVisibility, 100);
|
||||
|
||||
// Start listening for changes
|
||||
window.document.addEventListener('scroll', computeComponentsVisibilityDebounced, true);
|
||||
window.addEventListener('resize', computeComponentsVisibilityDebounced);
|
||||
|
||||
function computeComponentsVisibility() {
|
||||
_listeners.forEach(function(listener) {
|
||||
if( listener.isInViewport() ) {
|
||||
listener.onInViewport();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
26
src/store/actions/common.js
Normal file
26
src/store/actions/common.js
Normal file
@ -0,0 +1,26 @@
|
||||
var Util = require('../../util');
|
||||
|
||||
var LOAD_PROFILE = exports.LOAD_PROFILE = 'LOAD_PROFILE';
|
||||
var LOAD_PROFILE_SUCCESS = exports.LOAD_PROFILE_SUCCESS = 'LOAD_PROFILE_SUCCESS';
|
||||
var LOAD_PROFILE_FAILED = exports.LOAD_PROFILE_FAILED = 'LOAD_PROFILE_FAILED';
|
||||
|
||||
exports.loadProfile = function(profilePath) {
|
||||
|
||||
return function(dispatch, getState) {
|
||||
|
||||
dispatch({ type: LOAD_PROFILE, profilePath: profilePath });
|
||||
|
||||
return Util.System.loadJSON(profilePath)
|
||||
.then(function(profile) {
|
||||
dispatch({ type: LOAD_PROFILE_SUCCESS, profile: profile });
|
||||
return profile;
|
||||
})
|
||||
.catch(function(err) {
|
||||
dispatch({ type: LOAD_PROFILE_FAILED, error: err });
|
||||
return err;
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
};
|
112
src/store/actions/edit.js
Normal file
112
src/store/actions/edit.js
Normal file
@ -0,0 +1,112 @@
|
||||
var Util = require('../../util');
|
||||
var path = require('path');
|
||||
var _ = require('lodash');
|
||||
|
||||
// Action types
|
||||
var LOAD_DESKTOP_APPS = exports.LOAD_PROFILE = 'LOAD_DESKTOP_APPS';
|
||||
var LOAD_DESKTOP_APPS_SUCCESS = exports.LOAD_DESKTOP_APPS_SUCCESS = 'LOAD_DESKTOP_APPS_SUCCESS';
|
||||
var LOAD_DESKTOP_APPS_FAILED = exports.LOAD_DESKTOP_APPS_FAILED = 'LOAD_DESKTOP_APPS_FAILED';
|
||||
|
||||
var SAVE_PROFILE = exports.SAVE_PROFILE = 'SAVE_PROFILE';
|
||||
var SAVE_PROFILE_SUCCESS = exports.SAVE_PROFILE_SUCCESS = 'SAVE_PROFILE_SUCCESS';
|
||||
var SAVE_PROFILE_FAILED = exports.SAVE_PROFILE_FAILED = 'SAVE_PROFILE_FAILED';
|
||||
|
||||
var MOVE_PROFILE_ITEM = exports.MOVE_PROFILE_ITEM = 'MOVE_PROFILE_ITEM';
|
||||
var ADD_PROFILE_ITEM = exports.ADD_PROFILE_ITEM = 'ADD_PROFILE_ITEM';
|
||||
var REMOVE_PROFILE_ITEM = exports.REMOVE_PROFILE_ITEM = 'REMOVE_PROFILE_ITEM';
|
||||
var USE_ICON_THEME = exports.USE_ICON_THEME = 'USE_ICON_THEME';
|
||||
var SELECT_PROFILE_ITEM = exports.SELECT_PROFILE_ITEM = 'SELECT_PROFILE_ITEM';
|
||||
var UPDATE_PROFILE_ITEM = exports.UPDATE_PROFILE_ITEM = 'UPDATE_PROFILE_ITEM';
|
||||
|
||||
// Actions creators
|
||||
|
||||
exports.loadDesktopApps = function() {
|
||||
return function(dispatch, getState) {
|
||||
|
||||
var baseDirs = global.process.env.XDG_DATA_DIRS.split(':').map(function(baseDir){
|
||||
return path.join(baseDir, 'applications');
|
||||
});
|
||||
|
||||
dispatch({ type: LOAD_DESKTOP_APPS });
|
||||
|
||||
return Util.DesktopApps.loadAllDesktopFiles(baseDirs)
|
||||
.then(function(desktopApps) {
|
||||
dispatch({ type: LOAD_DESKTOP_APPS_SUCCESS, desktopApps: desktopApps });
|
||||
})
|
||||
.catch(function(err) {
|
||||
dispatch({ type: LOAD_DESKTOP_APPS_FAILED, error: err });
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
exports.saveProfile = function(destPath, profile) {
|
||||
return function(dispatch, getState) {
|
||||
|
||||
dispatch({ type: SAVE_PROFILE, profile: profile, path: destPath });
|
||||
|
||||
var cleanedProfile = _.cloneDeep(profile);
|
||||
|
||||
Util.Tree.walk(cleanedProfile, function(item) {
|
||||
delete item.selected;
|
||||
delete item._key;
|
||||
});
|
||||
|
||||
return Util.System.saveJSON(destPath, cleanedProfile)
|
||||
.then(function() {
|
||||
dispatch({ type: SAVE_PROFILE_SUCCESS, profile: profile, path: destPath });
|
||||
})
|
||||
.catch(function(err) {
|
||||
dispatch({ type: SAVE_PROFILE_FAILED, error: err });
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
exports.useIconTheme = function(theme) {
|
||||
return {
|
||||
type: USE_ICON_THEME,
|
||||
theme: theme
|
||||
};
|
||||
};
|
||||
|
||||
exports.moveProfileItem = function(movedItem, targetItem) {
|
||||
return {
|
||||
type: MOVE_PROFILE_ITEM,
|
||||
movedItem: movedItem,
|
||||
targetItem: targetItem
|
||||
};
|
||||
};
|
||||
|
||||
exports.removeProfileItem = function(removedItem) {
|
||||
return {
|
||||
type: REMOVE_PROFILE_ITEM,
|
||||
removedItem: removedItem
|
||||
};
|
||||
};
|
||||
|
||||
exports.addProfileItem = function(newItem, targetItem) {
|
||||
return {
|
||||
type: ADD_PROFILE_ITEM,
|
||||
newItem: newItem,
|
||||
targetItem: targetItem
|
||||
};
|
||||
};
|
||||
|
||||
exports.selectProfileItem = function(item) {
|
||||
return {
|
||||
type: SELECT_PROFILE_ITEM,
|
||||
item: item
|
||||
};
|
||||
};
|
||||
|
||||
exports.updateProfileItem = function(item, key, value) {
|
||||
return {
|
||||
type: UPDATE_PROFILE_ITEM,
|
||||
item: item,
|
||||
key: key,
|
||||
value: value
|
||||
};
|
||||
};
|
3
src/store/actions/index.js
Normal file
3
src/store/actions/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
exports.launcher = require('./launcher');
|
||||
exports.edit = require('./edit');
|
||||
exports.common = require('./common');
|
26
src/store/actions/launcher.js
Normal file
26
src/store/actions/launcher.js
Normal file
@ -0,0 +1,26 @@
|
||||
var Util = require('../../util');
|
||||
|
||||
var RUN_APP = exports.RUN_APP = 'RUN_APP';
|
||||
var RUN_APP_SUCCESS = exports.RUN_APP_SUCCESS = 'RUN_APP_SUCCESS';
|
||||
var RUN_APP_FAILED = exports.RUN_APP_FAILED = 'RUN_APP_FAILED';
|
||||
|
||||
exports.runApp = function(execPath) {
|
||||
|
||||
return function(dispatch, getState) {
|
||||
|
||||
dispatch({ type: RUN_APP, execPath: execPath });
|
||||
|
||||
return Util.System.runApp(execPath, { clearFreeDesktopFlags: true })
|
||||
.then(function() {
|
||||
dispatch({ type: RUN_APP_SUCCESS, execPath: execPath });
|
||||
return execPath;
|
||||
})
|
||||
.catch(function(err) {
|
||||
dispatch({ type: RUN_APP_FAILED, error: err });
|
||||
return err;
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
};
|
18
src/store/index.js
Normal file
18
src/store/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
var redux = require('redux');
|
||||
var thunkMiddleware = require('redux-thunk');
|
||||
var reducers = require('./reducers');
|
||||
var loggerMiddleware = require('./middlewares/logger');
|
||||
|
||||
var createStore = redux.applyMiddleware(
|
||||
thunkMiddleware,
|
||||
loggerMiddleware
|
||||
)(redux.createStore);
|
||||
|
||||
var appReducer = redux.combineReducers({
|
||||
profile: reducers.profile,
|
||||
processOpts: reducers.processOpts,
|
||||
desktopApps: reducers.desktopApps,
|
||||
theme: reducers.theme
|
||||
});
|
||||
|
||||
module.exports = createStore(appReducer);
|
15
src/store/middlewares/logger.js
Normal file
15
src/store/middlewares/logger.js
Normal file
@ -0,0 +1,15 @@
|
||||
var debug = require('../../util/debug')('store:logger');
|
||||
|
||||
module.exports = function loggerMiddleware(store) {
|
||||
return function(next) {
|
||||
return function(action) {
|
||||
debug('Action %j', action);
|
||||
debug('Store current state %j', store.getState());
|
||||
next(action);
|
||||
debug('Store new state %j', store.getState());
|
||||
if(action.error) {
|
||||
console.error(action.error.stack || action.error);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
13
src/store/reducers/desktop-apps.js
Normal file
13
src/store/reducers/desktop-apps.js
Normal file
@ -0,0 +1,13 @@
|
||||
var actions = require('../actions');
|
||||
|
||||
module.exports = function(state, action) {
|
||||
|
||||
var desktopApps = state || [];
|
||||
|
||||
if( action.type === actions.edit.LOAD_DESKTOP_APPS_SUCCESS ) {
|
||||
desktopApps = action.desktopApps;
|
||||
}
|
||||
|
||||
return desktopApps;
|
||||
|
||||
};
|
3
src/store/reducers/index.js
Normal file
3
src/store/reducers/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
exports.desktopApps = require('./desktop-apps');
|
||||
exports.profile = require('./profile');
|
||||
exports.theme = require('./theme');
|
111
src/store/reducers/profile.js
Normal file
111
src/store/reducers/profile.js
Normal file
@ -0,0 +1,111 @@
|
||||
var _ = require('lodash');
|
||||
var actions = require('../actions');
|
||||
var tree = require('../../util/tree');
|
||||
|
||||
module.exports = function(oldProfile, action) {
|
||||
|
||||
var newProfile = oldProfile || { items: [] };
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
case actions.common.LOAD_PROFILE_SUCCESS:
|
||||
newProfile = _.cloneDeep(action.profile);
|
||||
break;
|
||||
|
||||
case actions.edit.MOVE_PROFILE_ITEM:
|
||||
newProfile = moveProfileItem(oldProfile, action.movedItem, action.targetItem);
|
||||
break;
|
||||
|
||||
case actions.edit.REMOVE_PROFILE_ITEM:
|
||||
newProfile = removeProfileItem(oldProfile, action.removedItem);
|
||||
break;
|
||||
|
||||
case actions.edit.ADD_PROFILE_ITEM:
|
||||
newProfile = addProfileItem(oldProfile, action.newItem, action.targetItem);
|
||||
break;
|
||||
|
||||
case actions.edit.UPDATE_PROFILE_ITEM:
|
||||
newProfile = updateProfileItem(oldProfile, action.item, action.key, action.value);
|
||||
break;
|
||||
|
||||
case actions.edit.SELECT_PROFILE_ITEM:
|
||||
newProfile = selectProfileItem(oldProfile, action.item);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if(newProfile) tree.walk(newProfile, ensureItemKey);
|
||||
|
||||
return newProfile;
|
||||
|
||||
};
|
||||
|
||||
function selectProfileItem(oldProfile, item) {
|
||||
var newProfile = _.cloneDeep(oldProfile);
|
||||
tree.walk(newProfile, function(currentItem) {
|
||||
delete currentItem.selected;
|
||||
if( _.isEqual(currentItem, item) ) {
|
||||
currentItem.selected = true;
|
||||
}
|
||||
});
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
function updateProfileItem(oldProfile, targetItem, key, value) {
|
||||
var newProfile = _.cloneDeep(oldProfile);
|
||||
var item = tree.find(newProfile, targetItem).item;
|
||||
item[key] = value;
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
function removeProfileItem(oldProfile, removedItem) {
|
||||
|
||||
var newProfile = _.cloneDeep(oldProfile);
|
||||
var parent = tree.find(newProfile, removedItem).parent;
|
||||
|
||||
parent.items = _.reject(parent.items, function(item) {
|
||||
return _.isEqual(item, removedItem);
|
||||
});
|
||||
|
||||
return newProfile;
|
||||
|
||||
}
|
||||
|
||||
function moveProfileItem(oldProfile, movedItem, targetItem) {
|
||||
|
||||
var newProfile = _.cloneDeep(oldProfile);
|
||||
var previousParent = tree.find(newProfile, movedItem).parent;
|
||||
var newParent = tree.find(newProfile, targetItem).item;
|
||||
|
||||
previousParent.items = _.reject(previousParent.items, function(item) {
|
||||
return _.isEqual(item, movedItem);
|
||||
});
|
||||
|
||||
newParent.items = newParent.items || [];
|
||||
newParent.items.push(_.cloneDeep(movedItem));
|
||||
|
||||
return newProfile;
|
||||
|
||||
}
|
||||
|
||||
function addProfileItem(oldProfile, newItem, targetItem) {
|
||||
|
||||
var newProfile = _.cloneDeep(oldProfile);
|
||||
var newParent = tree.find(newProfile, targetItem).item;
|
||||
|
||||
newParent.items = newParent.items || [];
|
||||
|
||||
newItem = _.cloneDeep(newItem);
|
||||
ensureItemKey(newItem);
|
||||
|
||||
newParent.items.push(newItem);
|
||||
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
var _inc = 0;
|
||||
function ensureItemKey(item) {
|
||||
if( item && !('_key' in item) ) {
|
||||
item._key = 'item_'+Date.now()+'_'+_inc++;
|
||||
}
|
||||
}
|
14
src/store/reducers/theme.js
Normal file
14
src/store/reducers/theme.js
Normal file
@ -0,0 +1,14 @@
|
||||
var actions = require('../actions');
|
||||
|
||||
module.exports = function(currentTheme, action) {
|
||||
|
||||
switch(action.type) {
|
||||
|
||||
case actions.edit.USE_ICON_THEME:
|
||||
return action.theme;
|
||||
|
||||
default:
|
||||
return currentTheme || null;
|
||||
}
|
||||
|
||||
};
|
29
src/util/cache.js
Normal file
29
src/util/cache.js
Normal file
@ -0,0 +1,29 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function Cache() {
|
||||
this._store = {};
|
||||
}
|
||||
|
||||
Cache.prototype.get = function(key) {
|
||||
key = this._serialize(key);
|
||||
return key in this._store ? this._store[key] : undefined;
|
||||
};
|
||||
|
||||
Cache.prototype.set = function(key, value) {
|
||||
key = this._serialize(key);
|
||||
this._store[key] = value;
|
||||
return this;
|
||||
};
|
||||
|
||||
Cache.prototype._serialize = function(mixedKey) {
|
||||
var json = JSON.stringify(mixedKey);
|
||||
return this._hash(json);
|
||||
};
|
||||
|
||||
Cache.prototype._hash = function(str) {
|
||||
var shasum = crypto.createHash('md5');
|
||||
shasum.update(str);
|
||||
return shasum.digest('hex');
|
||||
};
|
||||
|
||||
module.exports = Cache;
|
2
src/util/const.js
Normal file
2
src/util/const.js
Normal file
@ -0,0 +1,2 @@
|
||||
exports.EDIT_MODE = 'edit';
|
||||
exports.LAUNCHER_MODE = 'launcher';
|
7
src/util/debug.js
Normal file
7
src/util/debug.js
Normal file
@ -0,0 +1,7 @@
|
||||
var debug = require('debug');
|
||||
var util = require('util');
|
||||
|
||||
module.exports = function createLogger(namespace) {
|
||||
var logger = debug('pitaya:'+namespace);
|
||||
return logger;
|
||||
};
|
297
src/util/desktop-apps.js
Normal file
297
src/util/desktop-apps.js
Normal file
@ -0,0 +1,297 @@
|
||||
var path = require('path');
|
||||
var System = require('./system');
|
||||
var debug = require('./debug')('desktop-apps');
|
||||
var Cache = require('./cache');
|
||||
var promises = require('./promises');
|
||||
|
||||
// Constants
|
||||
var ICON_REALPATH_REGEX = /\..+$/;
|
||||
var ICON_THEMES_ROOTDIR = '/usr/share/icons';
|
||||
var PIXMAPS_ICONS_ROOTDIR = '/usr/share/pixmaps';
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.then(function(filePaths) {
|
||||
|
||||
return promises.seq(filePaths, function(path) {
|
||||
return exports.loadDesktopFile(path);
|
||||
})
|
||||
.then(function(contents) {
|
||||
return contents.map(function(content, i) {
|
||||
return { content: content, path: filePaths[i] };
|
||||
});
|
||||
})
|
||||
;
|
||||
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all the desktop files in the subdirectories of given dirs
|
||||
*
|
||||
* @param Array[String] baseDirs
|
||||
* @return Promise
|
||||
*/
|
||||
exports.findAllDesktopFiles = function(baseDirs) {
|
||||
|
||||
if(!Array.isArray(baseDirs)) {
|
||||
baseDirs = [baseDirs];
|
||||
}
|
||||
|
||||
return promises.seq(baseDirs, function(baseDir) {
|
||||
return System.findFiles('**/*.desktop', {cwd: baseDir, realpath: true});
|
||||
})
|
||||
.then(function(apps) {
|
||||
return uniq(flatten(apps));
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a .desktop file ans return its parsed content
|
||||
*
|
||||
* @param string filePath
|
||||
* @return Promise
|
||||
*/
|
||||
exports.loadDesktopFile = function(filePath) {
|
||||
return System.loadINIFile(filePath);
|
||||
};
|
||||
|
||||
var iconCache = new Cache();
|
||||
|
||||
/**
|
||||
* Find the absolute path of a desktop icon
|
||||
*
|
||||
* @param string iconPath
|
||||
* @return Promise
|
||||
*/
|
||||
exports.findIcon = function(iconName, themeName, size, themeIgnore) {
|
||||
|
||||
var cachedIcon = iconCache.get([iconName, themeName, size]);
|
||||
|
||||
if(cachedIcon) {
|
||||
debug('Icon %s:%s:%s found in cache !', iconName, themeName, size);
|
||||
return Promise.resolve(cachedIcon);
|
||||
}
|
||||
|
||||
themeIgnore = themeIgnore || [];
|
||||
if(themeIgnore.indexOf(themeIgnore) !== -1) {
|
||||
debug('Theme %s already processed, ignoring...', themeName);
|
||||
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 || [];
|
||||
return promises.seq(themes, function(theme) {
|
||||
return exports.findIcon(iconName, theme, size, themeIgnore);
|
||||
})
|
||||
.then(exports._selectBestIcon)
|
||||
;
|
||||
})
|
||||
.then(_cacheIcon)
|
||||
;
|
||||
}
|
||||
|
||||
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)
|
||||
.then(function(iconPath) {
|
||||
|
||||
if(iconPath) return iconPath;
|
||||
|
||||
return exports.findPixmapsIcon(iconName);
|
||||
|
||||
})
|
||||
;
|
||||
|
||||
})
|
||||
.then(_cacheIcon)
|
||||
;
|
||||
|
||||
|
||||
function _cacheIcon(iconPath) {
|
||||
iconCache.set([iconName, themeName, size], iconPath);
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return promises.seq(parents, function(themeName) {
|
||||
return exports.findIcon(iconName, themeName, size, themeIgnore);
|
||||
})
|
||||
.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.findPixmapsIcon = function(iconName) {
|
||||
var filePattern = iconName+'.{svg,png}';
|
||||
debug('Looking for pixmap icon %s', filePattern);
|
||||
return System.findFiles(filePattern, {cwd: PIXMAPS_ICONS_ROOTDIR})
|
||||
.then(function(iconPaths) {
|
||||
iconPaths = iconPaths.map(function(iconPath) {
|
||||
return path.join(PIXMAPS_ICONS_ROOTDIR, iconPath);
|
||||
});
|
||||
return exports._selectBestIcon(iconPaths);
|
||||
})
|
||||
;
|
||||
};
|
||||
|
||||
exports.findIconThemes = function() {
|
||||
return System.findFiles('*/', {cwd: ICON_THEMES_ROOTDIR, realpath: true})
|
||||
.then(function(files) {
|
||||
return files.map(function(f) {
|
||||
return path.basename(f);
|
||||
});
|
||||
})
|
||||
;
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// 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]);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return arr.reduce(function(result, item) {
|
||||
if(result.indexOf(item) === -1) {
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
}
|
7
src/util/index.js
Normal file
7
src/util/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
exports.System = require('./system');
|
||||
exports.DesktopApps = require('./desktop-apps');
|
||||
exports.Cache = require('./cache');
|
||||
exports.Debug = require('./debug');
|
||||
exports.Tree = require('./tree');
|
||||
exports.Const = require('./const');
|
||||
exports.Promises = require('./promises');
|
31
src/util/promises.js
Normal file
31
src/util/promises.js
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
exports.seq = function(items, generator) {
|
||||
|
||||
var results = [];
|
||||
var p = Promise.resolve();
|
||||
|
||||
for(var i = 0, len = items.length; i < len; ++i) {
|
||||
p = p.then(generateNextHandler(items[i], i === 0))
|
||||
}
|
||||
|
||||
return p.then(function(lastResult) {
|
||||
results.push(lastResult);
|
||||
return results;
|
||||
});
|
||||
|
||||
// Internal helper
|
||||
function generateNextHandler(item, ignoreResult) {
|
||||
return function(prevResult) {
|
||||
if(!ignoreResult) results.push(prevResult);
|
||||
return generator(item, prevResult);
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.delay = function(delay) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
};
|
103
src/util/system.js
Normal file
103
src/util/system.js
Normal file
@ -0,0 +1,103 @@
|
||||
var fs = require('fs');
|
||||
var cp = require('child_process');
|
||||
var glob = require('glob');
|
||||
var ini = require('ini');
|
||||
var Cache = require('./cache');
|
||||
|
||||
/**
|
||||
* Load a JSON file
|
||||
*
|
||||
* @param filePath The path of the json file
|
||||
* @return Promise
|
||||
*/
|
||||
exports.loadJSON = 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.saveJSON = function(filePath, obj) {
|
||||
|
||||
var jsonStr = JSON.stringify(obj, null, 2);
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.writeFile(filePath, jsonStr, function(err) {
|
||||
if(err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.clearFreeDesktopFlags = function(exec) {
|
||||
return exec.replace(/%[uUdDfFnNickvm]/g, '');
|
||||
};
|
||||
|
||||
exports.runApp = function(execPath, opts) {
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
if(opts.clearFreeDesktopFlags) {
|
||||
execPath = exports.clearFreeDesktopFlags(execPath);
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
cp.exec(execPath, function(err) {
|
||||
if(err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var _searchCache = new Cache();
|
||||
|
||||
exports.findFiles = function(pattern, opts) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
var cachedResult = _searchCache.get([pattern, opts]);
|
||||
if( cachedResult !== undefined) {
|
||||
return resolve(cachedResult);
|
||||
}
|
||||
|
||||
glob(pattern, opts, function(err, files) {
|
||||
if(err) return reject(err);
|
||||
_searchCache.set([pattern, opts], files);
|
||||
return resolve(files);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.exists = function(filePath) {
|
||||
return new Promise(function(resolve) {
|
||||
fs.exists(filePath, resolve);
|
||||
});
|
||||
};
|
53
src/util/tree.js
Normal file
53
src/util/tree.js
Normal file
@ -0,0 +1,53 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
// Tree manipulation helpers
|
||||
|
||||
exports.walk = function(branch, func, parent) {
|
||||
|
||||
if(!branch) return;
|
||||
|
||||
var breakHere = func(branch, parent);
|
||||
|
||||
if(breakHere) return breakHere;
|
||||
|
||||
var items = branch.items;
|
||||
|
||||
if(!items) return;
|
||||
|
||||
for( var i = 0, item = items[i]; (item = items[i]); i++ ) {
|
||||
breakHere = exports.walk(item, func, branch);
|
||||
if(breakHere) return breakHere;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.find = function(tree, obj) {
|
||||
|
||||
var result;
|
||||
|
||||
exports.walk(tree, function(item, parent) {
|
||||
if( _.isEqual(item, obj) ) {
|
||||
result = {item: item, parent: parent};
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
exports.matches = function(tree, obj) {
|
||||
|
||||
var results = [];
|
||||
|
||||
var matches = _.matches(obj);
|
||||
|
||||
exports.walk(tree, function(item) {
|
||||
if( matches(item) ) {
|
||||
results.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
};
|
Reference in New Issue
Block a user