Merge branch 'feature/edit-mode' into develop

This commit is contained in:
wpetit 2015-10-07 13:07:29 +02:00
commit f7be43c558
45 changed files with 2305 additions and 253 deletions

View File

@ -4,7 +4,7 @@ var path = require('path');
module.exports = function(grunt) {
var NW_VERSION = '0.12.2';
var NW_VERSION = '0.12.3';
var BUILD_DIR = 'build';
var BUILD_TARGETS = {
linux_ia32: true,

View File

@ -16,7 +16,7 @@ git clone https://forge.cadoles.com/wpetit/pitaya.git
cd pitaya
git checkout develop
npm install
npm start
DEBUG=pitaya* npm start
```
## Options

View File

@ -6,7 +6,6 @@ html, body {
padding: 0;
margin: 0;
font-family: 'Droid Sans', 'Ubuntu Sans', sans-serif;
background: url('../img/background.png') no-repeat;
background-size: contain;
background-position: center;
background-color: rgb(34, 107, 160);
@ -16,6 +15,11 @@ html, body {
overflow-x: hidden;
}
.alert.alert-default {
border-radius: 4px;
border: 1px solid #ddd;
}
/* Launcher View */
.launcher {
@ -23,6 +27,7 @@ html, body {
width: 100%;
height: 100%;
flex-direction: column;
background: url('../img/background.png') no-repeat;
}
.launcher .category-header {
@ -48,7 +53,6 @@ html, body {
}
.launcher ul.apps-list {
display: block;
margin: 0;
padding: 0;
display: flex;
@ -73,7 +77,8 @@ html, body {
transition: 150ms linear;
position: relative;
overflow: hidden;
min-width: 150px;
width: 125px;
height: 125px;
}
.launcher li.app-item::after {
@ -101,6 +106,105 @@ html, body {
display: block;
text-align: center;
color: white;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 90%;
overflow: hidden;
margin: auto;
}
/* Edit View */
.edit {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
}
.edit .menu-bar {
padding: 5px 10px;
}
.edit .menu-bar button {
margin-right: 3px;
}
.edit .workspace {
display: flex;
flex-direction: row;
padding: 10px;
flex: 3;
}
.edit .left-menu {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
}
.edit .item-form {
display: flex;
flex-direction: row;
flex: 2;
}
.edit .item-form > form {
width: 100%;
}
.edit .apps-list .icon-theme-selector > select {
width: 100%;
}
.edit .apps-list ul.desktop-apps {
list-style: none;
padding: 0;
height: 100%;
margin: 10px 0 0 0;
padding: 0 10px 0 0;
}
.edit .apps-list li.desktop-app {
}
.edit .desktop-app > .app-icon {
height: 35px;
width: 35px;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
.edit .profile-tree {
flex: 3;
padding: 0 5px;
}
.edit .profile-tree ul {
list-style: none;
margin: 0;
}
.edit .profile-tree > .tree-item > ul {
padding: 0;
}
.edit .profile-tree .tree-item .alert {
padding: 5px;
margin-bottom: 2px;
}
.edit .profile-tree .tree-item .app-icon {
height: 25px;
margin-right: 5px;
}
.edit .app-item-edit {
flex: 1;
padding: 0 5px;
}
/* Animations */

View File

@ -2,30 +2,30 @@
"items": [
{
"label": "Level 1",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"icon": "chromium-browser",
"items": [
{
"label": "Level 2-1",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"icon": "chromium-browser",
"items": [
{
"label": "Chromium Browser",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"label": "Chromium Browser 1",
"icon": "chromium-browser",
"exec": "/usr/bin/chromium-browser"
}
]
},
{
"label": "Level 2-2",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"icon": "chromium-browser",
"items": [
{
"label": "Level 3-1",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"icon": "chromium-browser",
"items": [
{
"label": "Chromium Browser",
"icon": "/usr/share/icons/Mint-X/apps/48/chromium-browser.png",
"label": "Chromium Browser 2",
"icon": "chromium-browser",
"exec": "/usr/bin/chromium-browser"
}
]

61
img/default-icon.svg Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 512 512"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
width="100%"
height="100%"
sodipodi:docname="default-icon.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="850"
id="namedview8"
showgrid="false"
inkscape:zoom="0.65186406"
inkscape:cx="59.383922"
inkscape:cy="222.95767"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M0 0h512v512H0z"
fill="transparent"
stroke="#fff"
stroke-width="0"
id="path4" />
<path
d="M256 16C123.45 16 16 123.45 16 256s107.45 240 240 240 240-107.45 240-240S388.55 16 256 16zm0 60c99.41 0 180 80.59 180 180s-80.59 180-180 180S76 355.41 76 256 156.59 76 256 76zm-80.625 60c-.97-.005-2.006.112-3.063.313v-.032c-18.297 3.436-45.264 34.743-33.375 46.626l73.157 73.125-73.156 73.126c-14.63 14.625 29.275 58.534 43.906 43.906L256 299.906l73.156 73.156c14.63 14.628 58.537-29.28 43.906-43.906l-73.156-73.125 73.156-73.124c14.63-14.625-29.275-58.5-43.906-43.875L256 212.157l-73.156-73.125c-2.06-2.046-4.56-3.015-7.47-3.03z"
fill="#fff"
id="path6"
style="fill:#a4a4a4;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

154
img/hourglass.svg Normal file
View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50px"
height="50px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
class="uil-hourglass"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="hourglass.svg">
<metadata
id="metadata34">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs32" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="850"
id="namedview30"
showgrid="false"
inkscape:zoom="13.350176"
inkscape:cx="-10.530194"
inkscape:cy="27.889305"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g6" />
<rect
x="0"
y="0"
width="100"
height="100"
fill="none"
class="bk"
id="rect4" />
<g
id="g6">
<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"
id="path8"
style="fill:#a4a4a4;fill-opacity:1" />
<clipPath
id="uil-hourglass-clip1">
<rect
x="15"
y="20"
width="70"
height="25"
class="clip"
id="rect11">
<animate
attributeName="height"
from="25"
to="0"
dur="1s"
repeatCount="indefinite"
vlaues="25;0;0"
keyTimes="0;0.5;1"
id="animate13" />
<animate
attributeName="y"
from="20"
to="45"
dur="1s"
repeatCount="indefinite"
vlaues="20;45;45"
keyTimes="0;0.5;1"
id="animate15" />
</rect>
</clipPath>
<clipPath
id="uil-hourglass-clip2">
<rect
x="15"
y="55"
width="70"
height="25"
class="clip"
id="rect18">
<animate
attributeName="height"
from="0"
to="25"
dur="1s"
repeatCount="indefinite"
vlaues="0;25;25"
keyTimes="0;0.5;1"
id="animate20" />
<animate
attributeName="y"
from="80"
to="55"
dur="1s"
repeatCount="indefinite"
vlaues="80;55;55"
keyTimes="0;0.5;1"
id="animate22" />
</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"
id="path24"
style="fill:#ffffff;fill-opacity:1" />
<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"
id="path26"
style="fill:#ffffff;fill-opacity:1" />
<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"
id="animateTransform28" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -2,12 +2,13 @@
<head>
<title>Lanceur - Pitaya</title>
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<!-- Application root element -->
<div id="pitaya"></div>
<!-- Scripts -->
<script type="text/javascript">
// React context detection workaround

View File

@ -1,147 +1,43 @@
var React = require('react');
var minimist = require('minimist');
var gui = global.window.require('nw.gui');
var Util = require('./util');
var CategoryHeader = require('./components/category-header.jsx');
var AppList = require('./components/app-list.jsx');
var AnimateMixin = require('./mixins/animate');
// Internal constants
var DEFAULT_PROFILE = './default-profile.json';
var PROCESS_OPTS = minimist(gui.App.argv);
var LauncherView = require('./components/launcher/launcher-view.jsx');
var EditView = require('./components/edit/edit-view.jsx');
var Provider = require('react-redux').Provider;
var connect = require('react-redux').connect;
var store = require('./store');
// Main component
var App = React.createClass({
mixins: [AnimateMixin],
getInitialState: function() {
return {
currentItemPath: '',
currentItem: null
};
},
componentDidMount: function() {
// Load profile on component mount
Util.System.loadJSONFile(PROCESS_OPTS.profile || DEFAULT_PROFILE)
.then(function(profile) {
this.setState({ profile: profile, currentItem: profile, currentItemPath: '' });
}.bind(this))
;
},
render: function() {
var currentItem = this.state.currentItem;
var items = currentItem ? currentItem.items : [];
var currentItemPath = this.state.currentItemPath;
var editMode = this.props.processOpts.edit || false;
var header = currentItemPath !== '' ?
( <CategoryHeader
onBackClick={this.onBackClick}
item={currentItem}
itemPath={currentItemPath} /> ) :
null
;
var view = editMode ? <EditView /> : <LauncherView />;
return (
<div className="launcher">
{header}
<AppList ref="appList"
items={items}
parentPath={currentItemPath}
onItemClick={this.onItemClick} />
<div id="pitaya">
{view}
</div>
);
},
onBackClick: function(itemPath) {
var parentPath = this._normalizeItemPath(itemPath).slice(0, -1);
var parentItem = this._getItemByPath(parentPath);
this.play(this.refs.appList, 'slide-out-right 250ms ease-in-out')
.then(function() {
this.setState({currentItem: parentItem, currentItemPath: parentPath.join('.')});
return this.play(this.refs.appList, 'slide-in-left 250ms ease-in-out');
}.bind(this))
;
},
onItemClick: function(evt, itemPath, item) {
if(item.exec) {
console.info('Launching application "'+item.exec+'"...');
evt.currentTarget.classList.add('pulse');
Util.System.runApp(item.exec)
.then(function() {
evt.currentTarget.classList.remove('pulse');
})
.catch(function(err) {
evt.currentTarget.classList.remove('pulse');
})
;
} else {
this.play(this.refs.appList, 'slide-out-left 250ms ease-in-out')
.then(function() {
this.setState({ currentItemPath: itemPath, currentItem: item });
return this.play(this.refs.appList, 'slide-in-right 250ms ease-in-out');
}.bind(this))
;
}
},
_getItemByPath: function(itemPath, rootItem) {
rootItem = rootItem || this.state.profile;
itemPath = this._normalizeItemPath(itemPath);
var itemIndex = itemPath[0];
if(itemIndex === undefined) {
return rootItem;
}
if(!('items' in rootItem)) {
return undefined;
}
var subItem = rootItem.items[itemIndex];
if(itemPath.length === 0) {
return subItem;
}
return this._getItemByPath(itemPath.slice(1), subItem);
},
_normalizeItemPath: function(itemPath) {
if( Array.isArray(itemPath) ) return itemPath;
if((typeof itemPath === 'string' && itemPath.length === 0) || !itemPath) return [];
return itemPath.split('.').reduce(function(arr, index) {
if(index !== '') {
arr.push(+index);
}
return arr;
}, []);
}
});
var rootEl = document.getElementById('pitaya');
React.render(<App />, rootEl);
// 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
);

View File

@ -0,0 +1,70 @@
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;
return (
<img src={icon} className="app-icon" />
);
},
_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) {
self.setState({ icon: iconPath });
})
;
}
});

View File

@ -0,0 +1,55 @@
var React = require('react');
var Util = require('../../util');
var AppIcon = require('../common/app-icon.jsx');
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);

View File

@ -0,0 +1,33 @@
var React = require('react');
var Util = require('../../util');
var DesktopAppItem = require('./desktop-app-item.jsx');
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;

View File

@ -0,0 +1,74 @@
var React = require('react');
var connect = require('react-redux').connect;
var ProfileTree = require('./profile-tree.jsx');
var DesktopAppList = require('./desktop-app-list.jsx');
var ItemForm = require('./item-form.jsx');
var IconThemeSelector = require('./icon-theme-selector.jsx');
var ProfileMenu = require('./profile-menu.jsx');
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">
<div className="menu-bar">
<ProfileMenu />
</div>
<div className="workspace">
<div className="left-menu">
<IconThemeSelector onThemeSelected={this.handleThemeSelect} />
<DesktopAppList
theme={this.props.theme}
desktopApps={this.props.desktopApps}
onItemDropped={this.handleItemDrop} />
</div>
<ProfileTree />
<ItemForm item={this.props.selectedItem} onItemChange={this.handleItemChange} />
</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));
}
});
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));

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

View File

@ -0,0 +1,77 @@
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
});
}
},
render: function() {
var state = this.state;
return (
<div className="item-form">
<form>
<div className="form-group">
<label>Label</label>
<input type="text" className="form-control"
placeholder="Label"
value={state.label}
onChange={this.handleChange.bind(this, 'label')} />
</div>
<div className="form-group">
<label>Icon</label>
<input type="text" className="form-control"
placeholder="Icon"
value={state.icon}
onChange={this.handleChange.bind(this, 'icon')} />
</div>
<div className="form-group">
<label>Exec</label>
<input type="text" className="form-control"
placeholder="Exec" value={state.exec}
onChange={this.handleChange.bind(this, 'exec')} />
</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;

View File

@ -0,0 +1,79 @@
var React = require('react');
var connect = require('react-redux').connect;
var actions = require('../../store/actions');
var ProfileMenu = React.createClass({
render: function() {
return (
<div className="profile-menu">
<input ref="fileInput" style={{display: 'none'}} filter=".json" type="file" />
<button className="btn btn-default" onClick={this.handleOpenClick}>Ouvrir</button>
<button className="btn btn-primary" onClick={this.handleSaveClick}>Enregistrer</button>
</div>
);
},
handleOpenClick: function() {
var dispatch = this.props.dispatch;
this.showFileDialog()
.then(function(profilePath) {
dispatch(actions.common.loadProfile(profilePath));
})
;
},
handleSaveClick: function() {
var dispatch = this.props.dispatch;
var profile = this.props.profile;
var profilePath = this.props.profilePath;
var promise = profilePath ? Promise.resolve(profilePath) : this.showFileDialog(true);
promise.then(function(profilePath) {
dispatch(actions.edit.saveProfile(profilePath, profile));
});
},
showFileDialog: function(saveAs) {
var fileInput = this.refs.fileInput.getDOMNode();
// Toggle 'save as' feature
if(saveAs) {
fileInput.nwsaveas = true;
} else {
fileInput.removeAttribute('nwsaveas');
}
return new Promise(function(resolve, reject) {
fileInput.addEventListener('change', handleChange, false);
fileInput.click();
function handleChange(evt) {
fileInput.removeEventListener('change', handleChange);
var value = this.value;
this.value = null;
resolve(value);
}
});
}
});
function select(state) {
return {
profile: state.profile,
profilePath: state.profilePath
};
}
module.exports = connect(select)(ProfileMenu);

View File

@ -0,0 +1,90 @@
var React = require('react');
var connect = require('react-redux').connect;
var actions = require('../../store/actions');
var TreeItem = require('./tree-item.jsx');
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}
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}
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));
}
});
function select(state) {
return {
profile: state.profile,
theme: state.theme
};
}
module.exports = connect(select)(ProfileTree);

View File

@ -0,0 +1,96 @@
var React = require('react/addons');
var classNames = require('classnames');
var AppIcon = require('../common/app-icon.jsx');
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>
</div>
));
},
handleClick: function(evt) {
evt.preventDefault();
this.props.onItemClicked(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)
);

View File

@ -1,4 +1,5 @@
var React = require('react');
var AppIcon = require('../common/app-icon.jsx');
module.exports = React.createClass({
@ -19,7 +20,7 @@ module.exports = React.createClass({
render: function() {
return (
<li className="app-item" onClick={this._onItemClick}>
<img className="app-icon" src={this.props.item.icon} />
<AppIcon icon={this.props.item.icon} theme={null} />
<span className="app-label">{this.props.item.label}</span>
</li>
);

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

@ -0,0 +1,151 @@
var React = require('react');
var CategoryHeader = require('./category-header.jsx');
var AppList = require('./app-list.jsx');
var AnimateMixin = require('../mixins/animate');
var actions = require('../../store/actions');
var connect = require('react-redux').connect;
var debug = require('../../util/debug')('launcher-view');
var DEFAULT_PROFILE = './default-profile.json';
var LauncherView = React.createClass({
mixins: [AnimateMixin],
getInitialState: function() {
return {
currentItemPath: '',
currentItem: null
};
},
componentDidMount: function() {
var profilePath = this.props.processOpts.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
onBackClick={this.onBackClick}
item={currentItem}
itemPath={currentItemPath} /> ) :
null
;
return (
<div className="launcher">
{header}
<AppList ref="appList"
items={items}
parentPath={currentItemPath}
onItemClick={this.onItemClick} />
</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);

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

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

96
js/store/actions/edit.js Normal file
View File

@ -0,0 +1,96 @@
var Util = require('../../util');
var path = require('path');
// 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 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 });
return Util.System.saveJSON(destPath, profile)
.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.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
};
};

View File

@ -0,0 +1,3 @@
exports.launcher = require('./launcher');
exports.edit = require('./edit');
exports.common = require('./common');

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

19
js/store/index.js Normal file
View File

@ -0,0 +1,19 @@
var redux = require('redux');
var thunkMiddleware = require('redux-thunk');
var loggerMiddleware = require('redux-logger');
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);

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

View 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;
};

View File

@ -0,0 +1,4 @@
exports.desktopApps = require('./desktop-apps');
exports.profile = require('./profile');
exports.processOpts = require('./process-opts');
exports.theme = require('./theme');

View File

@ -0,0 +1,8 @@
var minimist = require('minimist');
var gui = global.window.require('nw.gui');
var opts = minimist(gui.App.argv);
module.exports = function(state, action) {
return opts;
};

View File

@ -0,0 +1,89 @@
var _ = require('lodash');
var actions = require('../actions');
var tree = require('../../util/tree');
module.exports = function(oldProfile, action) {
switch(action.type) {
case actions.common.LOAD_PROFILE_SUCCESS:
var newProfile = _.cloneDeep(action.profile);
tree.walk(newProfile, ensureItemKey);
return newProfile;
case actions.edit.MOVE_PROFILE_ITEM:
return moveProfileItem(oldProfile, action.movedItem, action.targetItem);
case actions.edit.ADD_PROFILE_ITEM:
return addProfileItem(oldProfile, action.newItem, action.targetItem);
case actions.edit.UPDATE_PROFILE_ITEM:
return updateProfileItem(oldProfile, action.item, action.key, action.value);
case actions.edit.SELECT_PROFILE_ITEM:
return selectProfileItem(oldProfile, action.item);
default:
return oldProfile || null;
}
};
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 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++;
}
}

View 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
js/util/cache.js Normal file
View 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;

17
js/util/debug.js Normal file
View File

@ -0,0 +1,17 @@
var debug = require('debug');
var util = require('util');
module.exports = function createLogger(namespace) {
var logger = debug('pitaya:'+namespace);
var isNWContext = 'window' in global;
var console = isNWContext ? global.window.console : global.console;
logger.log = function() {
if(isNWContext) {
console.log.apply(console, arguments);
} else {
var str = util.format.apply(util, arguments);
console.log(str);
}
};
return logger;
};

302
js/util/desktop-apps.js Normal file
View File

@ -0,0 +1,302 @@
var path = require('path');
var System = require('./system');
var debug = require('./debug')('desktop-apps');
var Cache = require('./cache');
// 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) {
var promises = filePaths.map(function(path) {
return exports.loadDesktopFile(path);
});
return Promise.all(promises)
.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];
}
var promises = baseDirs.map(function(baseDir) {
return System.findFiles('**/*.desktop', {cwd: baseDir, realpath: true});
});
return Promise.all(promises)
.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 || [];
var promises = themes.map(function(theme) {
return exports.findIcon(iconName, theme, size, themeIgnore);
});
return Promise.all(promises)
.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);
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.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(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);
};
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;
}, []);
}

View File

@ -1,104 +0,0 @@
(function(Pitaya) {
"use strict";
var ini = require('ini');
var glob = require('glob');
var path = require('path');
var fs = require('fs');
var FS = Pitaya.FS = {};
FS.loadAllDesktopFiles = function(rootDirs) {
return FS.findAllDesktopFiles(rootDirs)
.then(function(filePaths) {
var promises = filePaths.map(function(path) {
return FS.loadDesktopFile(path);
});
return Promise.all(promises)
.then(function(desktopFiles) {
return desktopFiles.map(function(desktop, i) {
return {
path: filePaths[i],
desktop: desktop
};
});
})
;
})
;
};
FS.findAllDesktopFiles = function(rootDirs) {
if(!Array.isArray(rootDirs)) {
rootDirs = [rootDirs];
}
var promises = rootDirs.map(function(rootDir) {
var globPath = path.join(rootDir, '**/*.desktop');
return new Promise(function(resolve, reject) {
glob(globPath, function(err, files) {
if(err) return reject(err);
return resolve(files);
});
});
});
return Promise.all(promises)
.then(function(apps) {
return uniq(flatten(apps));
})
;
};
FS.loadDesktopFile = function(filePath) {
return new Promise(function(resolve, reject) {
fs.readFile(filePath, 'utf8', function(err, content) {
if(err) return reject(err);
try {
var decoded = ini.decode(content);
return resolve(decoded);
} catch(err) {
return reject(err);
}
});
});
};
// Array helpers
function flatten(arr) {
return arr.reduce(function(result, item) {
result = result.concat.apply(result, Array.isArray(item) ? flatten(item) : [item]);
return result;
}, []);
}
function uniq(arr) {
return arr.reduce(function(result, item) {
if(result.indexOf(item) === -1) {
result.push(item);
}
return result;
}, []);
}
}(Pitaya = global.Pitaya || {}));
Pitaya.FS.loadAllDesktopFiles(['/usr/share/applications', '/home/william/.local/share/applications'])
.then(function(apps) {
console.log(apps[5])
})
;

View File

@ -1 +1,4 @@
exports.System = require('./system');
exports.DesktopApps = require('./desktop-apps');
exports.Cache = require('./cache');
exports.Debug = require('./debug');

View File

@ -1,5 +1,8 @@
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
@ -7,7 +10,7 @@ var cp = require('child_process');
* @param filePath The path of the json file
* @return Promise
*/
exports.loadJSONFile = function(filePath) {
exports.loadJSON = function(filePath) {
return new Promise(function(resolve, reject) {
fs.readFile(filePath, 'utf8', function(err, fileContent) {
if(err) return reject(err);
@ -21,7 +24,51 @@ exports.loadJSONFile = function(filePath) {
});
};
exports.runApp = function(execPath) {
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);
@ -29,3 +76,28 @@ exports.runApp = function(execPath) {
});
});
};
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
js/util/tree.js Normal file
View 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;
};

View File

@ -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,10 +23,18 @@
"kiosk": false
},
"dependencies": {
"bootstrap": "^3.3.5",
"classnames": "^2.1.3",
"debug": "^2.2.0",
"glob": "^5.0.14",
"ini": "^1.3.4",
"lodash": "^3.10.1",
"minimist": "^1.1.3",
"node-jsx": "^0.13.3",
"react": "^0.13.3"
"react": "^0.13.3",
"react-dnd": "^1.1.5",
"react-redux": "^2.0.0",
"redux": "^2.0.0",
"redux-thunk": "^0.1.0"
}
}

239
res/pitaya-mockup.ep Normal file
View File

@ -0,0 +1,239 @@
<?xml version="1.0"?>
<Document xmlns="http://www.evolus.vn/Namespace/Pencil"><Properties/><Pages><Page><Properties><Property name="name">Edit Mode</Property><Property name="id">1441104668808_1122</Property><Property name="width">1102</Property><Property name="height">666</Property><Property name="dimBackground"/><Property name="transparentBackground">true</Property><Property name="backgroundColor">#FFFFFFFF</Property><Property name="fid">edit_mode</Property><Property name="background">transparent</Property></Properties><Content><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:combobox" id="7114bc8e8ca8451fa3c57b46fab50dad" transform="matrix(1,0,0,1,13,625)"><p:metadata><p:property name="box"><![CDATA[207,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[Mint-X]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[0,1]]></p:property></p:metadata>
<g p:name="rect" id="d2311828f1aa478889cae089716f5baa" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="4f2b560150af4f36b1890d818e1b6c0d" d="M 0 0 C 61 1 121 1 182 0 C 182 8 182 17 182 25 C 121 25 61 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<g p:name="fillrect" id="fdd4551eb03143b78dfe3955570be1b4" style="fill: rgb(204, 204, 204); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line2" id="0e36e25050124edcbe380ad0a3f63bcc" d="M 182 0 C 190 0 199 0 207 0 C 207 8 207 17 207 25 C 199 25 190 25 182 25 C 182 17 182 8 182 0 z"/>
</g>
<path d="M 190.15 10 C 193 10 196 10 198.85 10 C 197 13 196 15 194.5 17.5 C 193 15 192 12 190.15 10 z" style="fill:black;stroke-linejoin: round;" p:name="triangle" id="78be00fb1e174ecd90a5aba17d59a68c"/>
<text p:name="text" id="a5323c5bd771465eb7e4c37b6f5261cf" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(5,17)">Mint-X</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:box" id="0e851b74adf24423bd07b93c74b7c614" transform="matrix(1,0,0,1,13,16)"><p:metadata><p:property name="box"><![CDATA[1072,46]]></p:property><p:property name="textPadding"><![CDATA[53.599999999999994,3.066666666666667]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="bb0b13444c0c44919d01a6f6a2383d62" transform="translate(0.5,0.5)" d="M 0 0 C 357 1 715 1 1072 0 C 1072 15 1072 31 1072 46 C 715 48 357 48 0 46 C 0 31 0 15 0 0 z"/>
<text p:name="text" id="a9107e00a6ae48cd85f60e7841d7fb30" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(536,23)"/>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:button" id="1701028dc9d940a9bcdd3b25b3183315" transform="matrix(1,0,0,1,24,27)"><p:metadata><p:property name="box"><![CDATA[90,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="default"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#CCCCCCFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[New Profile]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="aee462ee9f0244d2ad07358b24346621" style="fill: rgb(204, 204, 204); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="b92536dd93084b188a07601fa846eb72" d="M 0 0 C 30 1 60 1 90 0 C 90 8 90 17 90 25 C 60 25 30 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="d26421470b1a4893b5c62ced74f4e1fe" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(9,17)">New Profile</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:button" id="84f41c4dae7f4693ba65473e8bdfcd3c" transform="matrix(1,0,0,1,982,27)"><p:metadata><p:property name="box"><![CDATA[90,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="default"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#CCCCCCFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[Save]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="d20593d83200480eb06b2c7b982d1f82" style="fill: rgb(204, 204, 204); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="25ae69692030459a9c124a2b026c454d" d="M 0 0 C 30 1 60 1 90 0 C 90 8 90 17 90 25 C 60 25 30 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="d692ef27d2a74f2cb9d7d18750b7f0dd" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(31,17)">Save</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:button" id="61b0b43d552a4c05884b1c21cc56d09d" transform="matrix(1,0,0,1,123,27)"><p:metadata><p:property name="box"><![CDATA[90,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="default"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#CCCCCCFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[Open profile]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="357378ce7ec049c38868ae8f470aaac5" style="fill: rgb(204, 204, 204); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="6edefcb53cf94d9ba0e18b9845cbc4ec" d="M 0 0 C 30 1 60 1 90 0 C 90 8 90 17 90 25 C 60 25 30 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="236bcbbf2e644796813c1f3db6c6186d" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(7,17)">Open profile</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="3930a9ecedda4498be82313275f92a4f" transform="matrix(1,0,0,1,708,44)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Current profile: /home/cadoles/profile.json]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="7ba8f7dc7ad7417ba39f51f1e3850ac2" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="987f1764e98245308a0085df7a0e8275" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Current profile: /home/cadoles/profile.json</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="e997aafaf4284a578438d42c6caf3d00" transform="matrix(1,0,0,1,14,90)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[System applications]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="d7500ed904d54bf0b47d8841cb85bf76" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="293421c49e074362868048c1a7828520" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">System applications</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="1b4a1655a6034ba78d4512ff988be772" transform="matrix(1,0,0,1,13,617)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Icons theme]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="5cb139bb1bad489caa1e4d37dcf5dc43" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="d7a54f3e74a0442ca49599e90f75a198" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Icons theme</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:box" id="20499e72df0d4e699bb64506654701b8" transform="matrix(1,0,0,1,230,102)"><p:metadata><p:property name="box"><![CDATA[427,549]]></p:property><p:property name="textPadding"><![CDATA[21.35,36.599999999999994]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="26553811f2554d0c83b11fe5180af571" transform="translate(0.5,0.5)" d="M 0 0 C 142 0 285 0 427 0 C 426 183 426 366 427 549 C 285 550 142 550 0 549 C -1 366 -1 183 0 0 z"/>
<text p:name="text" id="6a9d38b178cc473cacdfc1d87f1ed1d2" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(214,275)"/>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="797ee418135f48e38b9e1a36a7df94d1" transform="matrix(1,0,0,1,230,88)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Profile Tree]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="1cea635e671c407ca725b6862e0519f4" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="d90b02c22c9740e887b7138dfb3485f3" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Profile Tree</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:box" id="9cb9a4b13cc64412b28585872a590aa2" transform="matrix(1,0,0,1,665,102)"><p:metadata><p:property name="box"><![CDATA[420,548]]></p:property><p:property name="textPadding"><![CDATA[20.99999999999999,36.53333333333333]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="89ea4601838b4091999922b4652e24a5" transform="translate(0.5,0.5)" d="M 0 0 C 140 -1 280 -1 420 0 C 421 183 421 365 420 548 C 280 549 140 549 0 548 C 0 365 0 183 0 0 z"/>
<text p:name="text" id="3597045f25c84f7c82ff7da6edd2ee09" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(210,274)"/>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="translate(239.5,112.5)" id="30576daae9f24ab8ace1af52b3426c0d" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="c88d9dddc94240e8ad763c7885050b57" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[407,42]]></p:property><p:property name="textPadding"><![CDATA[20.35,2.8000000000000003]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#3333FFFF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="1eee6d790718495b8169fadc4daab557" transform="translate(0.5,0.5)" d="M 0 0 C 136 0 271 0 407 0 C 407 14 407 28 407 42 C 271 41 136 41 0 42 C 0 28 0 14 0 0 z"/>
<text p:name="text" id="e6754813839845769594fc452a2432a7" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(51, 51, 255); fill-opacity: 1;" transform="translate(204,21)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="420212f63f734b7089bf21dbb7762531" transform="matrix(1,0,0,1,10.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[35,27]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#3333FFFF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="7828139ff7d24334b83c28e818c599d8" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="f58ef32e27624ea1951654bc6c9f9f26" d="M 0 0 C 12 0 23 0 35 0 C 35 9 35 18 35 27 C 23 27 12 27 0 27 C 0 18 0 9 0 0 z M 3 3 C 13 10 22 17 32 24 M 3 24 C 13 17 22 10 32 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="e5989f6e8e0c4c45a7c9285a4f076b92" d="M 2.5 10 L 32.5 10 L 32.5 17 L 2.5 17 z"/>
<foreignObject x="0" y="9" width="35" height="9" p:name="text" id="62a64c20e04f4f78a096a1fbf64b26a2" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(51, 51, 255); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">35 x 27</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="2244768af8184b25a90226e5b9b9f31b" transform="matrix(1,0,0,1,60.5,26.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Root category 1]]></p:property><p:property name="textColor"><![CDATA[#3333FFFF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="f6b7f9a5e5fc4607b88a40caa41ba2b2" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="8301fe26e78d4afca73a8e3196d26582" style="fill: rgb(51, 51, 255); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Root category 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="0899f11e1baf4d8998a1c80c00885d58" transform="matrix(1,0,0,1,666,90)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Node edit]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="80fa85dfc99a4c579e9c7e740903c946" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="b3f48b1c20704e7684d5252194207329" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Node edit</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:textbox" id="81ae8a66abba4781a336c6f2fa6fc9bb" transform="matrix(1,0,0,1,677,139)"><p:metadata><p:property name="box"><![CDATA[395,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[Root category 1]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[0,1]]></p:property></p:metadata>
<g p:name="rect" id="d9e90d5cc8924e699bb4f94775113d72" transform="translate(0.5,0.5)" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="9122f5fa319c42268570e4e766c252ba" transform="translate(0.5,0.5)" d="M 0 0 C 132 -1 263 -1 395 0 C 395 8 395 17 395 25 C 263 25 132 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="4819159aaf7848bebac10ef0f9a5a090" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(5,17)">Root category 1</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="ce513f5155064028bdb3194fc12ad4a8" transform="matrix(1,0,0,1,676,129)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Label]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="0477a2dcd69d4225a77efbd1146f3a88" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="df4bbe9f1b69476487cee49354a55596" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Label</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="37c290a8a9bd49ddb7cd522ab5b87bf2" transform="matrix(1,0,0,1,678,188)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Icon]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="54912555c77742d18cb97c58225c5b26" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="fbd4fb4fe6bf4f9da0535a30e60e6b34" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Icon</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:textbox" id="37b639ed2d634dec9929cdf681c30642" transform="matrix(1,0,0,1,677,198)"><p:metadata><p:property name="box"><![CDATA[395,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[my-category-icon.png]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[0,1]]></p:property></p:metadata>
<g p:name="rect" id="596e08737b124fea8e8b4a3a5811ad45" transform="translate(0.5,0.5)" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="00dab7646fa2477ca942c151cef49931" transform="translate(0.5,0.5)" d="M 0 0 C 132 -1 263 -1 395 0 C 395 8 395 17 395 25 C 263 25 132 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="23033a342b1a4785ad051dd0dd9683a7" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(5,17)">my-category-icon.png</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="0548b5c0f2a14913b9abee1e76fec64e" transform="matrix(1,0,0,1,679,245)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Exec]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="77aa83edcd4c4355ba7d770f0d20ed5f" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="a2e10a3f942d4998907a8f3933fc5213" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Exec</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:textbox" id="82dc5d83d7be46f391256d8b52e138ea" transform="matrix(1,0,0,1,678,255)"><p:metadata><p:property name="box"><![CDATA[395,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[0,1]]></p:property></p:metadata>
<g p:name="rect" id="b692e50f8e7041c494a073b7726e28ea" transform="translate(0.5,0.5)" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="327a23c3712443ef9ed64c33dd129066" transform="translate(0.5,0.5)" d="M 0 0 C 132 -1 263 -1 395 0 C 395 8 395 17 395 25 C 263 25 132 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="cae073243a964eddb27ad65a480e4aea" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(5,13)"> </text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:button" id="3b7d314d671a444f90972a23618f31a8" transform="matrix(1,0,0,1,534,71)"><p:metadata><p:property name="box"><![CDATA[123,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="default"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#CCCCCCFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[Add empty node]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="1779ce66a4af45968a436dac9185ffde" style="fill: rgb(204, 204, 204); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="6acc2c2dc10b4c66bfb91880dcfad267" d="M 0 0 C 41 1 82 1 123 0 C 123 8 123 17 123 25 C 82 25 41 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="60870178593549eca5b9a865d8c6b096" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(12,17)">Add empty node</text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,261.5,160.5)" id="ee44f94a38624ef3a93402b8463bbc48" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="5d400dd61cf54d03b41391fd4f0c4538" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[385,44]]></p:property><p:property name="textPadding"><![CDATA[19.25,2.9333333333333336]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="745f3bd5069c472db6e3bfd2f9df0caa" transform="translate(0.5,0.5)" d="M 0 0 C 128 -2 257 -2 385 0 C 385 15 385 29 385 44 C 257 42 128 42 0 44 C 0 29 0 15 0 0 z"/>
<text p:name="text" id="2d2beb1e2d03498b86e236d62cc4cd1b" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(193,22)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="a17c7f78e001454eb18837c57d859bd7" transform="matrix(1,0,0,1,9.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[33,28]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="c78869f1e07e40c593e428b2fb80980b" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="d1f1292cd88e4169ac2f76e5e51fd6b8" d="M 0 0 C 11 0 22 0 33 0 C 33 9 33 19 33 28 C 22 28 11 28 0 28 C 0 19 0 9 0 0 z M 3 3 C 12 10 21 18 30 25 M 3 25 C 12 18 21 10 30 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="bad62767355b4a2cb895becc9e1b0825" d="M 2.5 10.5 L 30.5 10.5 L 30.5 17.5 L 2.5 17.5 z"/>
<foreignObject x="0" y="10" width="33" height="9" p:name="text" id="bc2db73f34064d52b2ca40749f3d0a7f" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">33 x 28</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="17df8ae586bd4320a0343051a0f44b9d" transform="matrix(1,0,0,1,57.5,27.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub item 1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="912c5e0bd45e47649cd0ee3f76878660" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="a20fbadb77b047318d447cfcd2265d16" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub item 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,261.5,209.5)" id="30e1864d61284581844ced6b5adb07e7" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="582b157f8a7843b49a4b34c98b4e7567" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[385,44]]></p:property><p:property name="textPadding"><![CDATA[19.25,2.9333333333333336]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="d343eeb9a1174cf9b2bb362f06d83d9e" transform="translate(0.5,0.5)" d="M 0 0 C 128 1 257 1 385 0 C 385 15 385 29 385 44 C 257 45 128 45 0 44 C 0 29 0 15 0 0 z"/>
<text p:name="text" id="8d8a9a8b7ff747979a5b512de87807f7" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(193,22)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="022a7749fc70444b88d938c21c85f43a" transform="matrix(1,0,0,1,9.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[33,28]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="6a74baf1d60b496a8d7250f9b6f841c5" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="357e93596d0e4d1d9f0d8719814236a8" d="M 0 0 C 11 0 22 0 33 0 C 33 9 33 19 33 28 C 22 28 11 28 0 28 C 0 19 0 9 0 0 z M 3 3 C 12 10 21 18 30 25 M 3 25 C 12 18 21 10 30 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="3a1ca9747a31437bb56df60ef567a981" d="M 2.5 10.5 L 30.5 10.5 L 30.5 17.5 L 2.5 17.5 z"/>
<foreignObject x="0" y="10" width="33" height="9" p:name="text" id="8039ee0c3402430fb3a37514db5361f7" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">33 x 28</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="251baf9a8bcb4bf9b7e67324fe034e15" transform="matrix(1,0,0,1,57.5,27.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub item 2]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="ef105520c2be4c7f828a5e5d610fbc35" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="bbb2434f3aa2436bb16ca091c82954ec" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub item 2</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,239.5,258.5)" id="f47e4a18e9dc409dac11c635da500e9b" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="fae1c7261ea5438bbca6d36385047cec" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[407,42]]></p:property><p:property name="textPadding"><![CDATA[20.35,2.8000000000000003]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="d427d8db2e4745a7b346ec5841e8bf70" transform="translate(0.5,0.5)" d="M 0 0 C 136 0 271 0 407 0 C 407 14 407 28 407 42 C 271 41 136 41 0 42 C 0 28 0 14 0 0 z"/>
<text p:name="text" id="56c04232a63442adb859c9a1e6438257" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(204,21)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="f82cb87591ee440eb211aaeb192d5568" transform="matrix(1,0,0,1,10.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[35,27]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="310bef89d3774e5caa2600b8423d0937" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="69b214c2eb044531974de86e0fd84eb6" d="M 0 0 C 12 0 23 0 35 0 C 35 9 35 18 35 27 C 23 27 12 27 0 27 C 0 18 0 9 0 0 z M 3 3 C 13 10 22 17 32 24 M 3 24 C 13 17 22 10 32 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="eae7cb3976b0418b81e1509b319dcfcc" d="M 2.5 10 L 32.5 10 L 32.5 17 L 2.5 17 z"/>
<foreignObject x="0" y="9" width="35" height="9" p:name="text" id="63a300518fdf4e22a7fe821bbca83108" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">35 x 27</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="3113cf555e614101bb63d3c5430b4a1a" transform="matrix(1,0,0,1,60.5,26.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Root category 2]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="cc4604cb55fa454ca42813b0f3fa0d8e" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="b528725fc00f46f7989077bf02f82e06" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Root category 2</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,261.5,306.5)" id="a0d147428a25476c9c4b0b8f3486f868" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="83d3194d714947749348c58360f7bd87" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[385,44]]></p:property><p:property name="textPadding"><![CDATA[19.25,2.9333333333333336]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="f1cfee2e35aa4317be4f959ff647ece1" transform="translate(0.5,0.5)" d="M 0 0 C 128 -1 257 -1 385 0 C 385 15 385 29 385 44 C 257 45 128 45 0 44 C 0 29 0 15 0 0 z"/>
<text p:name="text" id="3818451a401149b3b2414be7404ab14e" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(193,22)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="dcd1f7ab0fd346998689eef595c3b65d" transform="matrix(1,0,0,1,9.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[33,28]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="d831eccf880a42febd3b044e23161732" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="70f658a4ea2f42b6a718b0c9b8f46d16" d="M 0 0 C 11 0 22 0 33 0 C 33 9 33 19 33 28 C 22 28 11 28 0 28 C 0 19 0 9 0 0 z M 3 3 C 12 10 21 17 30 25 M 3 25 C 12 18 21 10 30 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="e67d0fa03de245079bb7d2bb3578ddfe" d="M 2.5 10.5 L 30.5 10.5 L 30.5 17.5 L 2.5 17.5 z"/>
<foreignObject x="0" y="10" width="33" height="9" p:name="text" id="4fedef99f5bd4e36874c09a09008dd76" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">33 x 28</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="3c9e1d9aaf004c52883314a5c5eea4e8" transform="matrix(1,0,0,1,57.5,27.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub item 1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="9598b190d6db4ac0878e7416d0df24e3" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="748cbdc11b384053887750748de8e40b" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub item 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,261.5,356.5)" id="3b59ee009bf24263a23b309aa26f9e1d" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="e4636d3a25914ff68f6dd9a15cfa8ce5" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[385,42]]></p:property><p:property name="textPadding"><![CDATA[19.25,2.8000000000000003]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="aded3faec3db4551b5ed9178bc2a4d7e" transform="translate(0.5,0.5)" d="M 0 0 C 128 0 257 0 385 0 C 385 14 385 28 385 42 C 257 42 128 42 0 42 C 0 28 0 14 0 0 z"/>
<text p:name="text" id="c302174fd70943329a9594e115086e67" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(193,21)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="84df156db62c493ba2b4adfab472bd62" transform="matrix(1,0,0,1,9.5,6.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[33,27]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="b4d492671760479e9fcad4b6ef90b140" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="295883e714fe4924a3dd936797d7b680" d="M 0 0 C 11 0 22 0 33 0 C 33 9 33 18 33 27 C 22 27 11 27 0 27 C 0 18 0 9 0 0 z M 3 3 C 12 10 21 17 30 24 M 3 24 C 12 17 21 10 30 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="459afdc3a4174f2eb414935032a91cea" d="M 2.5 10 L 30.5 10 L 30.5 17 L 2.5 17 z"/>
<foreignObject x="0" y="9" width="33" height="9" p:name="text" id="82e823921ec043ad98c416c3175500c3" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">33 x 27</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="85790f2bdf6a4847bd66289b3918dafc" transform="matrix(1,0,0,1,57.5,26.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub category 1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="a09efdc8aeaa4bb09166c53e6a7f65a0" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="527cfca6be034ac998cbc5e02f972560" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub category 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,286.5,403.5)" id="d899c8a7d22447778f7d24d286b88e05" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="1a2312f63dfa4758936838b05a45a68c" transform="matrix(1,0,0,1,-0.5,0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[360,46]]></p:property><p:property name="textPadding"><![CDATA[18,3.066666666666667]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="453e2dd91d6a4012a106a943e8af980f" transform="translate(0.5,0.5)" d="M 0 0 C 120 -2 240 -2 360 0 C 360 15 360 31 360 46 C 240 45 120 45 0 46 C 0 31 0 15 0 0 z"/>
<text p:name="text" id="bde2c7f7db974c72b66073a518f6a6c8" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(180,23)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="1eb2d028c77546569954d99ff72ffe31" transform="matrix(1,0,0,1,9.5,7.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[31,29]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="9fdef7bf97f8490989365f6e1c1d6a8f" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="1203683b8d304baa9693af167a683d04" d="M 0 0 C 10 0 21 0 31 0 C 31 10 31 19 31 29 C 21 29 10 29 0 29 C 0 19 0 10 0 0 z M 3 3 C 11 11 20 18 28 26 M 3 26 C 11 18 20 11 28 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="54c1bf207b964b499932df225a3fbf32" d="M 2.5 11 L 28.5 11 L 28.5 18 L 2.5 18 z"/>
<foreignObject x="0" y="10" width="31" height="9" p:name="text" id="5148da41a20742ef8eb3867fb17a4707" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">31 x 29</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="da43f27cfa4a4e25bcd238dd2e249a1d" transform="matrix(1,0,0,1,53.5,27.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub sub item 1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="ee547d03791b46bc920ce69d247be7d8" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="6076079beae343eb8945238cf3920165" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub sub item 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,286.5,456.5)" id="abf3a65078de41838648d0ad752e624d" p:sizing-gow="407" p:sizing-goh="42"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="e872c97a04e042a484cb5f72c5c56d20" transform="matrix(1,0,0,1,-0.5,0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="407" p:sizing-oh="42"><p:metadata><p:property name="box"><![CDATA[360,46]]></p:property><p:property name="textPadding"><![CDATA[18,3.066666666666667]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="62df53d8cbfc459ea15298aeda6ee383" transform="translate(0.5,0.5)" d="M 0 0 C 120 -2 240 -2 360 0 C 360 15 360 31 360 46 C 240 45 120 45 0 46 C 0 31 0 15 0 0 z"/>
<text p:name="text" id="fd39594f5ae4486d98f97684570db1af" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(180,23)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="583ad06df8fb4625b6e567476cd59437" transform="matrix(1,0,0,1,9.5,7.5)" p:sizing-ox="10.5" p:sizing-oy="6.5" p:sizing-ow="35" p:sizing-oh="27"><p:metadata><p:property name="box"><![CDATA[31,29]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="bc4e7ea6887b4ae3b4291095044c3658" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="7eebb576a9154a409b1a38b04a751b05" d="M 0 0 C 10 0 21 0 31 0 C 31 10 31 19 31 29 C 21 29 10 29 0 29 C 0 19 0 10 0 0 z M 3 3 C 11 11 20 18 28 26 M 3 26 C 11 18 20 11 28 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="2290e43afac94c6795d9b9727561a941" d="M 2.5 11 L 28.5 11 L 28.5 18 L 2.5 18 z"/>
<foreignObject x="0" y="10" width="31" height="9" p:name="text" id="c4a0785ce2ba403e93c93079bd316796" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">31 x 29</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="2b62ca78a20248eebd9ec099106ae95a" transform="matrix(1,0,0,1,53.5,27.5)" p:sizing-ox="60.5" p:sizing-oy="14.5" p:sizing-ow="98" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Sub sub item 1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="b6555cd537e84a169ab8d789e0f8cc1a" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="56208111b8ae4b98ba510bbc71e668e2" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Sub sub item 1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:box" id="ad6b60b24208434bbdb4f4b8257149b2" transform="matrix(1,0,0,1,13,103)"><p:metadata><p:property name="box"><![CDATA[195,497]]></p:property><p:property name="textPadding"><![CDATA[9.75,33.133333333333326]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="2b4b10d1542c4ad59b93daea2e718e01" transform="translate(0.5,0.5)" d="M 0 0 C 65 -1 130 -1 195 0 C 194 166 194 331 195 497 C 130 497 65 497 0 497 C 1 331 1 166 0 0 z"/>
<text p:name="text" id="e4e8506d7d754b88a684b92feb536406" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(98,249)"/>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="translate(24.5,112.5)" id="3416a2f2b049432fb8193df31db9f7bb" p:sizing-gow="175" p:sizing-goh="35.75"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="a9836db52c5648d9a53f00f5bf89fa49" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="175" p:sizing-oh="35"><p:metadata><p:property name="box"><![CDATA[175,35]]></p:property><p:property name="textPadding"><![CDATA[8.75,2.3333333333333335]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="16a70d988beb4b8fb23e245c25a3b496" transform="translate(0.5,0.5)" d="M 0 0 C 58 2 117 2 175 0 C 175 12 175 23 175 35 C 117 36 58 36 0 35 C 0 23 0 12 0 0 z"/>
<text p:name="text" id="8b34ae6f750543369e10c5c48b91c225" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(88,18)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="378561df86d940e8b3073b3ac1c24433" transform="matrix(1,0,0,1,4.5,5.5)" p:sizing-ox="4.5" p:sizing-oy="5.5" p:sizing-ow="32" p:sizing-oh="26"><p:metadata><p:property name="box"><![CDATA[32,26]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="cdffda30759049c9a4d8f00096514615" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="4c224cfec9bf41bf92d775078bbfbe7e" d="M 0 0 C 11 0 21 0 32 0 C 32 9 32 17 32 26 C 21 26 11 26 0 26 C 0 17 0 9 0 0 z M 3 3 C 12 10 20 16 29 23 M 3 23 C 12 16 20 10 29 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="235adb2065cd469499afd64d61f1c1ba" d="M 2.5 9.5 L 29.5 9.5 L 29.5 16.5 L 2.5 16.5 z"/>
<foreignObject x="0" y="9" width="32" height="9" p:name="text" id="d06c2e46f69f4f45827c914ae064bbd1" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">32 x 26</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="bf71aaa140c64a088c89fb24c6017bc9" transform="matrix(1,0,0,1,42.5,23.5)" p:sizing-ox="41.5" p:sizing-oy="11.5" p:sizing-ow="48" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[App #1]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="f7712cbc7adb4d9686857655cfcc7b2d" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="cdbbd19b9e194e79965a834ea48fa504" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">App #1</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,24.5,154.5)" id="773e1f149e6740358d7f10f533a7856c" p:sizing-gow="175" p:sizing-goh="35.75"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="bbe78e7d77b64873a7c8a298e9635e0d" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="175" p:sizing-oh="35"><p:metadata><p:property name="box"><![CDATA[175,35]]></p:property><p:property name="textPadding"><![CDATA[8.75,2.3333333333333335]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="db366bd675064641b8067feef443685f" transform="translate(0.5,0.5)" d="M 0 0 C 58 2 117 2 175 0 C 175 12 175 23 175 35 C 117 36 58 36 0 35 C 0 23 0 12 0 0 z"/>
<text p:name="text" id="e9e724c12d414312ba30b8978737b737" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(88,18)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="e57cd7bde47047a099f3f552af1e3548" transform="matrix(1,0,0,1,4.5,5.5)" p:sizing-ox="4.5" p:sizing-oy="5.5" p:sizing-ow="32" p:sizing-oh="26"><p:metadata><p:property name="box"><![CDATA[32,26]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="c42027ed733d4dbf8122e8ad434e68f2" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="c11e7c8e4fdc4badbe016586897fe896" d="M 0 0 C 11 0 21 0 32 0 C 32 9 32 17 32 26 C 21 26 11 26 0 26 C 0 17 0 9 0 0 z M 3 3 C 12 10 20 16 29 23 M 3 23 C 12 16 20 10 29 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="5fce320e1eeb4671925048c6656a0d65" d="M 2.5 9.5 L 29.5 9.5 L 29.5 16.5 L 2.5 16.5 z"/>
<foreignObject x="0" y="9" width="32" height="9" p:name="text" id="26b617aff48f4c4ea7c1ca3057478cd5" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">32 x 26</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="1405e7f9091f44bb83997f1f07155c6a" transform="matrix(1,0,0,1,42.5,23.5)" p:sizing-ox="41.5" p:sizing-oy="11.5" p:sizing-ow="48" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[App #2]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="f5c265e17c3c412eaba0b30317252a72" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="a6caa89248a14c33a33f5409a706cfbc" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">App #2</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,24.5,195.5)" id="6f3944cf504b4713828a897c09cb6f1a" p:sizing-gow="175" p:sizing-goh="35.75"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="eaf4ecf43230490f9a238ee08be43527" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="175" p:sizing-oh="35"><p:metadata><p:property name="box"><![CDATA[175,35]]></p:property><p:property name="textPadding"><![CDATA[8.75,2.3333333333333335]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="1d347d95008d4fcd8dc849d85de10974" transform="translate(0.5,0.5)" d="M 0 0 C 58 2 117 2 175 0 C 175 12 175 23 175 35 C 117 36 58 36 0 35 C 0 23 0 12 0 0 z"/>
<text p:name="text" id="32649584b0454f2ab99fcc0f79bf6830" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(88,18)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="24c86e209b9448e5b7c89e751e05765c" transform="matrix(1,0,0,1,4.5,5.5)" p:sizing-ox="4.5" p:sizing-oy="5.5" p:sizing-ow="32" p:sizing-oh="26"><p:metadata><p:property name="box"><![CDATA[32,26]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="edaf16c6af4947cb91d9adf177d32bd4" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="6b9059f420c043bfb30343ab8e4e014a" d="M 0 0 C 11 0 21 0 32 0 C 32 9 32 17 32 26 C 21 26 11 26 0 26 C 0 17 0 9 0 0 z M 3 3 C 12 10 20 16 29 23 M 3 23 C 12 16 20 10 29 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="23f0512a35cc4567aaf7007eeb150ec4" d="M 2.5 9.5 L 29.5 9.5 L 29.5 16.5 L 2.5 16.5 z"/>
<foreignObject x="0" y="9" width="32" height="9" p:name="text" id="6fdab76c72ab4bf7a496a6d40f788270" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">32 x 26</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="2f3216c74ae64f94a8ac1fa9f784c0b2" transform="matrix(1,0,0,1,42.5,23.5)" p:sizing-ox="41.5" p:sizing-oy="11.5" p:sizing-ow="48" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[App #3]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="be6a8c086f9b4e9f8dd4437c97496116" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="8bfdc6a8f1a342829a677ab18fe69c10" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">App #3</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Group" xmlns:p="http://www.evolus.vn/Namespace/Pencil" transform="matrix(1,0,0,1,24.5,236.5)" id="7a2a9644ecad4ccd9fd935d327cd95c5" p:sizing-gow="175" p:sizing-goh="35.75"><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="2ff1da8b00594d838f2bc9bf039d7c1a" transform="matrix(1,0,0,1,-0.5,-0.5)" p:sizing-ox="0" p:sizing-oy="0" p:sizing-ow="175" p:sizing-oh="35"><p:metadata><p:property name="box"><![CDATA[175,35]]></p:property><p:property name="textPadding"><![CDATA[8.75,2.3333333333333335]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="acb98b417a634aa6a2c30dd413de2b35" transform="translate(0.5,0.5)" d="M 0 0 C 58 2 117 2 175 0 C 175 12 175 23 175 35 C 117 36 58 36 0 35 C 0 23 0 12 0 0 z"/>
<text p:name="text" id="c5efded4c6f048ee8c087c41de284c9f" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(88,18)"/>
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:image" id="bba12f27c56741bcbeb68d7390f2e270" transform="matrix(1,0,0,1,4.5,5.5)" p:sizing-ox="4.5" p:sizing-oy="5.5" p:sizing-ow="32" p:sizing-oh="26"><p:metadata><p:property name="box"><![CDATA[32,26]]></p:property><p:property name="text"><![CDATA[true]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[<br />]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|7px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[1,1]]></p:property></p:metadata>
<g p:name="rect" id="d4c864540d8845eaa5222168adbee011" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round;" p:name="line1" id="39c862e7d2994c41bb6a409864629244" d="M 0 0 C 11 0 21 0 32 0 C 32 9 32 17 32 26 C 21 26 11 26 0 26 C 0 17 0 9 0 0 z M 3 3 C 12 10 20 16 29 23 M 3 23 C 12 16 20 10 29 3"/>
</g>
<path style="stroke: none; fill: rgb(255, 255, 255); fill-opacity: 1; visibility: visible;" p:name="mask" id="a5c967844bed4cf0b22a32ec69fb1f04" d="M 2.5 9.5 L 29.5 9.5 L 29.5 16.5 L 2.5 16.5 z"/>
<foreignObject x="0" y="9" width="32" height="9" p:name="text" id="3dabe35f0fda4bc9a54eccf8455651a7" style="font-family: &quot;Comic Sans MS&quot;; font-size: 7px; font-weight: normal; font-style: normal; text-decoration: none; color: rgb(0, 0, 0); opacity: 1; text-align: center; visibility: visible;"><div xmlns="http://www.w3.org/1999/xhtml">32 x 26</div></foreignObject>
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="6c50ededf2854c62b6aaff8e231366f2" transform="matrix(1,0,0,1,42.5,23.5)" p:sizing-ox="41.5" p:sizing-oy="11.5" p:sizing-ow="48" p:sizing-oh="15"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[App #4]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="7787a81a2e7747c7a704fe6c99989674" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="80e821165efb4fad9a579d63ea3bf711" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">App #4</tspan></text>
</g></g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:line" id="699071ea1e234caeae0e6ca83d89f905" transform="matrix(1,0,0,1,680,305)"><p:metadata><p:property name="a"><![CDATA[0,0]]></p:property><p:property name="b"><![CDATA[390,-1]]></p:property><p:property name="mode"><![CDATA[free]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property></p:metadata>
<g p:name="rect" id="4f94f82182cc474f852cefdb119071ca" transform="translate(0.5,0.5)" style="stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke: #FFFFFF; stroke-opacity: 0; stroke-width: 8px; fill: none;" p:name="bg" id="d9566275ae6646c886ce9c838013819f" transform="translate(0.5,0.5)" d="M 0 0 C 130 -2 260 -2 390 -1"/>
<path style="stroke-linejoin: round; fill: none;" p:name="line1" id="c9f1758559b2478285197a4fd672b7e2" transform="translate(0.5,0.5)" d="M 0 0 C 130 -1 260 -2 390 -1"/>
</g>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="d8d6b422531e476fa2eaea5ca6bbd6ef" transform="matrix(1,0,0,1,678,329)"><p:metadata><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="width"><![CDATA[100,0]]></p:property><p:property name="fixedWidth"><![CDATA[false]]></p:property><p:property name="label"><![CDATA[Background image]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textAlign"><![CDATA[0,0]]></p:property></p:metadata>
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="2b6e1189c8314db8bc0c69b344ab178e" width="0" height="0"/>
<text xml:space="preserve" p:name="text" id="2e69bfeea81d43f7929cbcd83d895035" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Background image</tspan></text>
</g><g xmlns="http://www.w3.org/2000/svg" p:type="Shape" xmlns:p="http://www.evolus.vn/Namespace/Pencil" p:def="Evolus.Sketchy.GUI:textbox" id="97bed2a12fa94dda9c3836df982dd7ef" transform="matrix(1,0,0,1,679,339)"><p:metadata><p:property name="box"><![CDATA[395,25]]></p:property><p:property name="disabled"><![CDATA[false]]></p:property><p:property name="fillColor"><![CDATA[#FFFFFFFF]]></p:property><p:property name="strokeColor"><![CDATA[#000000FF]]></p:property><p:property name="strokeStyle"><![CDATA[1|]]></p:property><p:property name="textContent"><![CDATA[]]></p:property><p:property name="textFont"><![CDATA['Comic Sans MS'|normal|normal|12px|none]]></p:property><p:property name="textColor"><![CDATA[#000000FF]]></p:property><p:property name="textAlign"><![CDATA[0,1]]></p:property></p:metadata>
<g p:name="rect" id="a11e9a6d463f402588f1cae0fa88fa87" transform="translate(0.5,0.5)" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;">
<path style="stroke-linejoin: round; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="1c148b86d13d4b5f98f5b2f607282e06" transform="translate(0.5,0.5)" d="M 0 0 C 132 -1 263 -1 395 0 C 395 8 395 17 395 25 C 263 25 132 25 0 25 C 0 17 0 8 0 0 z"/>
</g>
<text p:name="text" id="9059ede59c394831b79ddb169ca24f76" style="font-family: &quot;Comic Sans MS&quot;; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;" transform="translate(5,13)"> </text>
</g></Content></Page></Pages></Document>

BIN
res/pitaya-mockup.pdf Normal file

Binary file not shown.

35
test/desktop.js Normal file
View File

@ -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('phpmyadmin')
.then(function(iconPath) {
console.log('findIcon', iconPath);
test.done();
})
.catch(function(err) {
test.ifError(err);
test.done();
})
;
};