Skip to content

Commit 1b3c141

Browse files
committed
Added initial support for grid-based desktop iconview (#51)
1 parent e6ff448 commit 1b3c141

File tree

2 files changed

+149
-10
lines changed

2 files changed

+149
-10
lines changed

src/adapters/ui/iconview.js

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,39 @@
2929
*/
3030
import {EventEmitter} from '@osjs/event-emitter';
3131
import {h, app} from 'hyperapp';
32+
import {draggable, droppable} from '../../utils/dnd';
33+
import {emToPx} from '../../utils/dom';
3234
import {doubleTap} from '../../utils/input';
3335
import {pathJoin} from '../../utils/vfs';
3436

3537
const tapper = doubleTap();
3638

39+
//
40+
// FIXME: Excessive render on drop events
41+
//
42+
43+
// TODO: Needs real values
44+
const ICON_WIDTH = 5.0; // ems
45+
const ICON_HEIGHT = 6.5; // ems
46+
const ICON_MARGIN = 0.5; // ems
47+
3748
const validVfsDrop = data => data && data.path;
49+
const validInternalDrop = data => data && data.internal;
50+
51+
// TODO: Use internal storage
52+
const loadIconPositions = () => JSON.parse(
53+
localStorage.getItem('___osjs_iconview_positions') || '[]'
54+
);
55+
56+
// TODO: Use internal storage
57+
const saveIconPositions = positions =>
58+
localStorage.setItem('___osjs_iconview_positions', JSON.stringify(positions || []));
3859

3960
const onDropAction = actions => (ev, data, files, shortcut = true) => {
4061
if (validVfsDrop(data)) {
41-
actions.addEntry({entry: data, shortcut});
62+
actions.addEntry({entry: data, shortcut, ev});
63+
} else if(validInternalDrop(data)) {
64+
actions.moveEntry({entry: data.internal, ev});
4265
} else if (files.length > 0) {
4366
actions.uploadEntries(files);
4467
}
@@ -47,6 +70,67 @@ const onDropAction = actions => (ev, data, files, shortcut = true) => {
4770
const isRootElement = ev =>
4871
ev.target && ev.target.classList.contains('osjs-desktop-iconview__wrapper');
4972

73+
const calculateGridSizes = el => {
74+
const {offsetWidth, offsetHeight} = el;
75+
// TODO: Might cause reflow, do cache here
76+
const sizeX = emToPx(ICON_WIDTH) + (emToPx(ICON_MARGIN) * 2);
77+
const sizeY = emToPx(ICON_HEIGHT) + (emToPx(ICON_MARGIN) * 2);
78+
const cols = Math.floor(offsetWidth / sizeX);
79+
const rows = Math.floor(offsetHeight / sizeY);
80+
return [rows, cols, sizeX, sizeY];
81+
};
82+
83+
const calculateIconPositions = (entries, positions, cols) => {
84+
const savedPositions = entries.map(entry => {
85+
const key = entry.shortcut === false ? entry.filename : entry.shortcut;
86+
const found = positions.findIndex(s => s.key === key);
87+
return found === -1 ? undefined : positions[found].position;
88+
});
89+
90+
return entries.map((entry, index) => {
91+
const x = index % cols;
92+
const y = Math.floor(index / cols);
93+
const _position = savedPositions[index] || [x, y];
94+
95+
return Object.assign(entry, {_position});
96+
});
97+
};
98+
99+
const isIconPositionBusy = (ev, {entries, grid: {sizeX, sizeY}}) => {
100+
const col = Math.floor(ev.clientX / sizeX);
101+
const row = Math.floor(ev.clientY / sizeY);
102+
103+
return entries.findIndex(e => {
104+
return e._position[0] === col &&
105+
e._position[1] === row;
106+
}) !== -1;
107+
};
108+
109+
const createIconStyle = (entry, index, {grid: {enabled, sizeX, sizeY}}) => {
110+
const [left, top] = entry._position || [0, 0];
111+
112+
return enabled ? {
113+
position: 'absolute',
114+
top: String(top * sizeY) + 'px',
115+
left: String(left * sizeX) + 'px'
116+
} : {};
117+
};
118+
119+
const createGhostStyle = ({ghost, grid: {enabled, sizeX, sizeY}}) => {
120+
const style = {};
121+
if (ghost instanceof Event) {
122+
const col = Math.floor(ghost.clientX / sizeX);
123+
const row = Math.floor(ghost.clientY / sizeY);
124+
style.top = String(row * sizeY) + 'px';
125+
style.left = String(col * sizeX) + 'px';
126+
}
127+
128+
return Object.assign({
129+
position: enabled ? 'absolute' : undefined,
130+
display: ghost ? undefined : 'none'
131+
}, style);
132+
};
133+
50134
const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
51135
h('div', {
52136
class: 'osjs-desktop-iconview__wrapper',
@@ -80,6 +164,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
80164
}, [
81165
...state.entries.map((entry, index) => {
82166
return h('div', {
167+
style: createIconStyle(entry, index, state),
83168
class: 'osjs-desktop-iconview__entry' + (
84169
state.selected === index
85170
? ' osjs-desktop-iconview__entry--selected'
@@ -88,7 +173,12 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
88173
oncontextmenu: ev => actions.openContextMenu({ev, entry, index}),
89174
ontouchstart: ev => tapper(ev, () => actions.openEntry({ev, entry, index})),
90175
ondblclick: ev => actions.openEntry({ev, entry, index}),
91-
onclick: ev => actions.selectEntry({ev, entry, index})
176+
onclick: ev => actions.selectEntry({ev, entry, index}),
177+
oncreate: el => {
178+
draggable(el, {
179+
data: {internal: entry}
180+
});
181+
}
92182
}, [
93183
h('div', {
94184
class: 'osjs-desktop-iconview__entry__inner'
@@ -115,9 +205,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
115205
}),
116206
h('div', {
117207
class: 'osjs-desktop-iconview__entry osjs-desktop-iconview__entry--ghost',
118-
style: {
119-
display: state.ghost ? undefined : 'none'
120-
}
208+
style: createGhostStyle(state)
121209
})
122210
]);
123211

@@ -207,6 +295,10 @@ export class DesktopIconView extends EventEmitter {
207295
this.$root.style.left = `${rect.left}px`;
208296
this.$root.style.bottom = `${rect.bottom}px`;
209297
this.$root.style.right = `${rect.right}px`;
298+
299+
if (this.iconview) {
300+
this.iconview.resize();
301+
}
210302
}
211303

212304
_render(settings) {
@@ -238,20 +330,31 @@ export class DesktopIconView extends EventEmitter {
238330
this.core.$root.appendChild(this.$root);
239331

240332
const root = settings.path;
241-
const {droppable} = this.core.make('osjs/dnd');
242333
const {icon: fileIcon} = this.core.make('osjs/fs');
243334
const {icon: themeIcon} = this.core.make('osjs/theme');
244335
const {copy, readdir, readfile, writefile, unlink, mkdir} = this.core.make('osjs/vfs');
245336
const error = err => console.error(err);
246337
const shortcuts = createShortcuts(root, readfile, writefile);
247338
const read = readDesktopFolder(root, readdir, shortcuts);
248339

340+
const [rows, cols, sizeX, sizeY] = calculateGridSizes(this.$root);
341+
249342
this.iconview = app({
250343
selected: -1,
251344
entries: [],
252-
ghost: false
345+
positions: loadIconPositions(),
346+
ghost: false,
347+
grid: {
348+
enabled: settings.grid,
349+
rows,
350+
cols,
351+
sizeX,
352+
sizeY
353+
}
253354
}, {
254-
setEntries: entries => ({entries}),
355+
setEntries: entries => state => {
356+
return {entries: calculateIconPositions(entries, state.positions, state.grid.cols)};
357+
},
255358

256359
openDropContextMenu: ({ev, data, files}) => {
257360
this.createDropContextMenu(ev, data, files);
@@ -292,7 +395,7 @@ export class DesktopIconView extends EventEmitter {
292395
// TODO
293396
},
294397

295-
addEntry: ({entry, shortcut}) => (state, actions) => {
398+
addEntry: ({entry, shortcut, ev}) => (state, actions) => {
296399
const dest = `${root}/${entry.filename}`;
297400

298401
mkdir(root)
@@ -325,12 +428,47 @@ export class DesktopIconView extends EventEmitter {
325428
return {selected: -1};
326429
},
327430

431+
moveEntry: ({entry, ev}) => (state) => {
432+
if (!isIconPositionBusy(ev, state)) {
433+
const positions = state.positions;
434+
const key = entry.shortcut === false ? entry.filename : entry.shortcut;
435+
const found = positions.findIndex(s => s.key === key);
436+
const col = Math.floor(ev.clientX / sizeX);
437+
const row = Math.floor(ev.clientY / sizeY);
438+
const position = [col, row];
439+
const value = {key, position};
440+
441+
if (found !== -1) {
442+
positions[found] = value;
443+
} else {
444+
positions.push(value);
445+
}
446+
447+
saveIconPositions(positions);
448+
449+
return {
450+
positions,
451+
entries: calculateIconPositions(state.entries, positions, state.cols)
452+
};
453+
}
454+
return {};
455+
},
456+
328457
reload: () => (state, actions) => {
329458
read()
330459
.then(entries => entries.filter(e => e.filename !== '..'))
331460
.then(entries => actions.setEntries(entries));
332461
},
333462

463+
resize: () => ({grid: {enabled}}) => {
464+
const [rows, cols, sizeX, sizeY] = calculateGridSizes(this.$root);
465+
return {grid: {enabled, rows, cols, sizeX, sizeY}};
466+
},
467+
468+
toggleGrid: enabled => ({grid}) => {
469+
return {grid: Object.assign(grid, {enabled})};
470+
},
471+
334472
setGhost: ev => {
335473
return {ghost: ev};
336474
}

src/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ export const defaultConfiguration = {
180180
},
181181
iconview: {
182182
enabled: false,
183-
path: 'home:/.desktop'
183+
path: 'home:/.desktop',
184+
grid: true
184185
}
185186
}
186187
},

0 commit comments

Comments
 (0)