From fc84ad4abfcbf8655fc7a1b7feaff7e9a391a778 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 2 Oct 2018 22:00:21 -0700 Subject: [PATCH] Revert "Revert "Serialize to indexdb" (#664)" This reverts commit a053818c5e057ad4af6dd2acc9fede4f4d6a8df1. --- css/webamp.css | 3 + js/__snapshots__/serialization.test.ts.snap | 115 +++++++++ .../fixtures/serializedState/v1/balance.json | 111 ++++++++ .../serializedState/v1/double-mode.json | 111 ++++++++ .../serializedState/v1/equalizer-auto.json | 111 ++++++++ .../serializedState/v1/equalizer-band.json | 111 ++++++++ .../serializedState/v1/equalizer-on.json | 111 ++++++++ .../serializedState/v1/equalizer-preamp.json | 111 ++++++++ .../serializedState/v1/focused-window.json | 111 ++++++++ .../serializedState/v1/llama-mode.json | 111 ++++++++ .../v1/main-window-position.json | 111 ++++++++ .../fixtures/serializedState/v1/repeat.json | 111 ++++++++ .../fixtures/serializedState/v1/shuffle.json | 111 ++++++++ .../fixtures/serializedState/v1/volume.json | 111 ++++++++ .../serializedState/v1/window-hidden.json | 111 ++++++++ .../serializedState/v1/window-open.json | 111 ++++++++ .../v1/window-positions-are-relative.json | 111 ++++++++ .../serializedState/v1/window-shade.json | 111 ++++++++ .../serializedState/v1/window-size.json | 111 ++++++++ js/actionCreators/equalizer.ts | 13 +- js/actionCreators/index.ts | 28 +- js/actionCreators/windows.ts | 175 ++++++++++++- js/actionTypes.ts | 3 + js/components/App.js | 2 +- js/components/WindowManager.js | 220 ---------------- js/components/WindowManager.tsx | 161 ++++++++++++ js/hotkeys.ts | 12 +- js/index.js | 17 +- js/indexedDB.js | 41 +++ js/mediaMiddleware.ts | 45 +++- js/reducers/display.ts | 36 ++- js/reducers/equalizer.ts | 12 +- js/reducers/media.ts | 11 +- js/reducers/playlist.ts | 4 +- js/reducers/windows.ts | 131 ++++++++-- js/selectors.ts | 105 ++++++-- js/serialization.test.ts | 239 ++++++++++++++++++ js/serializedStates/v1Types.ts | 64 +++++ js/snapUtils.ts | 10 +- js/types.ts | 49 ++-- js/utils.ts | 95 ++++--- js/webampLazy.js | 80 +++--- package.json | 3 + redux.md | 19 ++ yarn.lock | 19 ++ 45 files changed, 3207 insertions(+), 392 deletions(-) create mode 100644 js/__snapshots__/serialization.test.ts.snap create mode 100644 js/__tests__/fixtures/serializedState/v1/balance.json create mode 100644 js/__tests__/fixtures/serializedState/v1/double-mode.json create mode 100644 js/__tests__/fixtures/serializedState/v1/equalizer-auto.json create mode 100644 js/__tests__/fixtures/serializedState/v1/equalizer-band.json create mode 100644 js/__tests__/fixtures/serializedState/v1/equalizer-on.json create mode 100644 js/__tests__/fixtures/serializedState/v1/equalizer-preamp.json create mode 100644 js/__tests__/fixtures/serializedState/v1/focused-window.json create mode 100644 js/__tests__/fixtures/serializedState/v1/llama-mode.json create mode 100644 js/__tests__/fixtures/serializedState/v1/main-window-position.json create mode 100644 js/__tests__/fixtures/serializedState/v1/repeat.json create mode 100644 js/__tests__/fixtures/serializedState/v1/shuffle.json create mode 100644 js/__tests__/fixtures/serializedState/v1/volume.json create mode 100644 js/__tests__/fixtures/serializedState/v1/window-hidden.json create mode 100644 js/__tests__/fixtures/serializedState/v1/window-open.json create mode 100644 js/__tests__/fixtures/serializedState/v1/window-positions-are-relative.json create mode 100644 js/__tests__/fixtures/serializedState/v1/window-shade.json create mode 100644 js/__tests__/fixtures/serializedState/v1/window-size.json delete mode 100644 js/components/WindowManager.js create mode 100644 js/components/WindowManager.tsx create mode 100644 js/indexedDB.js create mode 100644 js/serialization.test.ts create mode 100644 js/serializedStates/v1Types.ts create mode 100644 redux.md diff --git a/css/webamp.css b/css/webamp.css index 280a4d73..4b15138c 100644 --- a/css/webamp.css +++ b/css/webamp.css @@ -4,6 +4,9 @@ position: absolute; top: 0; left: 0; + right: 0; + bottom: 0; + overflow: hidden; } /* Prevent accidental highlighting */ diff --git a/js/__snapshots__/serialization.test.ts.snap b/js/__snapshots__/serialization.test.ts.snap new file mode 100644 index 00000000..a961707a --- /dev/null +++ b/js/__snapshots__/serialization.test.ts.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can serialize the initial state 1`] = ` +Object { + "display": Object { + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinColors": Array [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)", + ], + "skinCursors": null, + "skinGenLetterWidths": null, + "skinImages": Object { + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=", + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + }, + "skinPlaylistStyle": null, + "skinRegion": Object {}, + "visualizerStyle": 0, + }, + "equalizer": Object { + "auto": false, + "on": true, + "sliders": Object { + "1000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "170": 50, + "3000": 50, + "310": 50, + "60": 50, + "600": 50, + "6000": 50, + "preamp": 50, + }, + }, + "media": Object { + "balance": 0, + "repeat": false, + "shuffle": false, + "volume": 78, + }, + "version": 1, + "windows": Object { + "focused": "main", + "genWindows": Object { + "equalizer": Object { + "hidden": false, + "open": true, + "position": Object { + "x": 0, + "y": 0, + }, + "shade": false, + "size": Array [ + 0, + 0, + ], + }, + "main": Object { + "hidden": false, + "open": true, + "position": Object { + "x": 0, + "y": 0, + }, + "shade": false, + "size": Array [ + 0, + 0, + ], + }, + "playlist": Object { + "hidden": false, + "open": true, + "position": Object { + "x": 0, + "y": 0, + }, + "shade": false, + "size": Array [ + 0, + 0, + ], + }, + }, + "positionsAreRelative": true, + }, +} +`; diff --git a/js/__tests__/fixtures/serializedState/v1/balance.json b/js/__tests__/fixtures/serializedState/v1/balance.json new file mode 100644 index 00000000..2698d1b2 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/balance.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 40, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/double-mode.json b/js/__tests__/fixtures/serializedState/v1/double-mode.json new file mode 100644 index 00000000..f9193bd7 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/double-mode.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": true, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/equalizer-auto.json b/js/__tests__/fixtures/serializedState/v1/equalizer-auto.json new file mode 100644 index 00000000..09e579e9 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/equalizer-auto.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": true, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/equalizer-band.json b/js/__tests__/fixtures/serializedState/v1/equalizer-band.json new file mode 100644 index 00000000..c0eb62e6 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/equalizer-band.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 100, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/equalizer-on.json b/js/__tests__/fixtures/serializedState/v1/equalizer-on.json new file mode 100644 index 00000000..585a01ea --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/equalizer-on.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": false, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/equalizer-preamp.json b/js/__tests__/fixtures/serializedState/v1/equalizer-preamp.json new file mode 100644 index 00000000..65115b13 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/equalizer-preamp.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 10 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/focused-window.json b/js/__tests__/fixtures/serializedState/v1/focused-window.json new file mode 100644 index 00000000..0ec9358b --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/focused-window.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "equalizer" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/llama-mode.json b/js/__tests__/fixtures/serializedState/v1/llama-mode.json new file mode 100644 index 00000000..f761ace8 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/llama-mode.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": true, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/main-window-position.json b/js/__tests__/fixtures/serializedState/v1/main-window-position.json new file mode 100644 index 00000000..bb95446e --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/main-window-position.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 100, + "y": 100 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/repeat.json b/js/__tests__/fixtures/serializedState/v1/repeat.json new file mode 100644 index 00000000..ec16fdaa --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/repeat.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": true + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/shuffle.json b/js/__tests__/fixtures/serializedState/v1/shuffle.json new file mode 100644 index 00000000..18496b2d --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/shuffle.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": true, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/volume.json b/js/__tests__/fixtures/serializedState/v1/volume.json new file mode 100644 index 00000000..ca3bb494 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/volume.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 50, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/window-hidden.json b/js/__tests__/fixtures/serializedState/v1/window-hidden.json new file mode 100644 index 00000000..87555494 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/window-hidden.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": true, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/window-open.json b/js/__tests__/fixtures/serializedState/v1/window-open.json new file mode 100644 index 00000000..63b1a7dd --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/window-open.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": false, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/window-positions-are-relative.json b/js/__tests__/fixtures/serializedState/v1/window-positions-are-relative.json new file mode 100644 index 00000000..c522410c --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/window-positions-are-relative.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": false, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 100, + "y": 100 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/window-shade.json b/js/__tests__/fixtures/serializedState/v1/window-shade.json new file mode 100644 index 00000000..9f1baa59 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/window-shade.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": true, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/__tests__/fixtures/serializedState/v1/window-size.json b/js/__tests__/fixtures/serializedState/v1/window-size.json new file mode 100644 index 00000000..bbc244e2 --- /dev/null +++ b/js/__tests__/fixtures/serializedState/v1/window-size.json @@ -0,0 +1,111 @@ +{ + "version": 1, + "media": { + "volume": 78, + "balance": 0, + "shuffle": false, + "repeat": false + }, + "equalizer": { + "on": true, + "auto": false, + "sliders": { + "60": 50, + "170": 50, + "310": 50, + "600": 50, + "1000": 50, + "3000": 50, + "6000": 50, + "12000": 50, + "14000": 50, + "16000": 50, + "preamp": 50 + } + }, + "display": { + "visualizerStyle": 0, + "doubled": false, + "llama": false, + "marqueeStep": 0, + "skinImages": { + "EQ_PREAMP_LINE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAAABCAYAAADpXEERAAAAE0lEQVQoU2Pcdfruf4ZRMKRDAAD1lwNjTqcaUQAAAABJRU5ErkJggg==", + "EQ_GRAPH_LINE_COLORS": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAATCAYAAABRC2cZAAAAR0lEQVQYV2O4rCT9n+F9kOJ/hvfViv8ZHkzSQCE2afxneH/HEJm49Nr0PwOYWPLIAkp0PjL4z1B41uQ/Q9QGnf8MWrPEIAQANWYwvnlToNIAAAAASUVORK5CYII=" + }, + "skinCursors": null, + "skinRegion": {}, + "skinGenLetterWidths": null, + "skinColors": [ + "rgb(0,0,0)", + "rgb(24,33,41)", + "rgb(239,49,16)", + "rgb(206,41,16)", + "rgb(214,90,0)", + "rgb(214,102,0)", + "rgb(214,115,0)", + "rgb(198,123,8)", + "rgb(222,165,24)", + "rgb(214,181,33)", + "rgb(189,222,41)", + "rgb(148,222,33)", + "rgb(41,206,16)", + "rgb(50,190,16)", + "rgb(57,181,16)", + "rgb(49,156,8)", + "rgb(41,148,0)", + "rgb(24,132,8)", + "rgb(255,255,255)", + "rgb(214,214,222)", + "rgb(181,189,189)", + "rgb(160,170,175)", + "rgb(148,156,165)", + "rgb(150,150,150)" + ], + "skinPlaylistStyle": null + }, + "windows": { + "positionsAreRelative": true, + "genWindows": { + "main": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "equalizer": { + "size": [ + 0, + 0 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + }, + "playlist": { + "size": [ + 4, + 5 + ], + "open": true, + "hidden": false, + "shade": false, + "position": { + "x": 0, + "y": 0 + } + } + }, + "focused": "main" + } +} \ No newline at end of file diff --git a/js/actionCreators/equalizer.ts b/js/actionCreators/equalizer.ts index fb4b8988..2ef90bc0 100644 --- a/js/actionCreators/equalizer.ts +++ b/js/actionCreators/equalizer.ts @@ -1,6 +1,11 @@ import { BANDS } from "../constants"; -import { SET_EQ_ON, SET_EQ_OFF, SET_BAND_VALUE } from "../actionTypes"; +import { + SET_EQ_ON, + SET_EQ_OFF, + SET_BAND_VALUE, + SET_EQ_AUTO +} from "../actionTypes"; import { Band, Dispatchable } from "../types"; const BAND_SNAP_DISTANCE = 10; @@ -52,3 +57,9 @@ export function toggleEq(): Dispatchable { } }; } + +export function toggleEqAuto(): Dispatchable { + return (dispatch, getState) => { + dispatch({ type: SET_EQ_AUTO, value: !getState().equalizer.auto }); + }; +} diff --git a/js/actionCreators/index.ts b/js/actionCreators/index.ts index 76844812..5142e3b8 100644 --- a/js/actionCreators/index.ts +++ b/js/actionCreators/index.ts @@ -5,9 +5,12 @@ import { CLOSE_REQUESTED, MINIMIZE_WINAMP, SET_FOCUS, - UNSET_FOCUS + UNSET_FOCUS, + LOAD_SERIALIZED_STATE } from "../actionTypes"; import { Dispatchable } from "../types"; +import { ensureWindowsAreOnScreen } from "./windows"; +import { SerializedStateV1 } from "../serializedStates/v1Types"; export { toggleDoubleSizeMode, @@ -19,7 +22,15 @@ export { setWindowSize, toggleWindow, updateWindowPositions, - toggleMainWindowShadeMode + toggleMainWindowShadeMode, + centerWindowsInContainer, + centerWindowsInView, + resetWindowSizes, + browserWindowSizeChanged, + ensureWindowsAreOnScreen, + stackWindows, + toggleLlamaMode, + setFocusedWindow } from "./windows"; export { play, @@ -43,7 +54,8 @@ export { setEqToMid, setEqToMin, setPreamp, - toggleEq + toggleEq, + toggleEqAuto } from "./equalizer"; export { addTracksFromReferences, @@ -108,3 +120,13 @@ export function setFocus(input: string): Dispatchable { export function unsetFocus(): Dispatchable { return { type: UNSET_FOCUS }; } + +export function loadSerializedState( + // In the future this type should be the union of all versioned types. + serializedState: SerializedStateV1 +): Dispatchable { + return dispatch => { + dispatch({ type: LOAD_SERIALIZED_STATE, serializedState }); + dispatch(ensureWindowsAreOnScreen()); + }; +} diff --git a/js/actionCreators/windows.ts b/js/actionCreators/windows.ts index 830e43c0..244cc7da 100644 --- a/js/actionCreators/windows.ts +++ b/js/actionCreators/windows.ts @@ -1,10 +1,6 @@ -import { - getWindowGraph, - getWindowSizes, - getWindowPositions -} from "../selectors"; +import * as Selectors from "../selectors"; -import { objectMap } from "../utils"; +import * as Utils from "../utils"; import { UPDATE_WINDOW_POSITIONS, TOGGLE_DOUBLESIZE_MODE, @@ -12,7 +8,11 @@ import { TOGGLE_WINDOW, CLOSE_WINDOW, TOGGLE_WINDOW_SHADE_MODE, - SET_WINDOW_VISIBILITY + SET_WINDOW_VISIBILITY, + BROWSER_WINDOW_SIZE_CHANGED, + RESET_WINDOW_SIZES, + TOGGLE_LLAMA_MODE, + SET_FOCUSED_WINDOW } from "../actionTypes"; import { getPositionDiff, SizeDiff } from "../resizeUtils"; @@ -28,12 +28,12 @@ import { Action, Dispatchable, WindowId, WindowPositions } from "../types"; function withWindowGraphIntegrity(action: Action): Dispatchable { return (dispatch, getState) => { const state = getState(); - const graph = getWindowGraph(state); - const originalSizes = getWindowSizes(state); + const graph = Selectors.getWindowGraph(state); + const originalSizes = Selectors.getWindowSizes(state); dispatch(action); - const newSizes = getWindowSizes(getState()); + const newSizes = Selectors.getWindowSizes(getState()); const sizeDiff: SizeDiff = {}; for (const window of Object.keys(newSizes)) { const original = originalSizes[window]; @@ -45,9 +45,9 @@ function withWindowGraphIntegrity(action: Action): Dispatchable { } const positionDiff = getPositionDiff(graph, sizeDiff); - const windowPositions = getWindowPositions(state); + const windowPositions = Selectors.getWindowPositions(state); - const newPositions = objectMap(windowPositions, (position, key) => + const newPositions = Utils.objectMap(windowPositions, (position, key) => applyDiff(position, positionDiff[key]) ); @@ -59,6 +59,10 @@ export function toggleDoubleSizeMode(): Dispatchable { return withWindowGraphIntegrity({ type: TOGGLE_DOUBLESIZE_MODE }); } +export function toggleLlamaMode(): Dispatchable { + return { type: TOGGLE_LLAMA_MODE }; +} + export function toggleEqualizerShadeMode(): Dispatchable { return withWindowGraphIntegrity({ type: TOGGLE_WINDOW_SHADE_MODE, @@ -92,6 +96,10 @@ export function showWindow(windowId: WindowId): Dispatchable { return { type: SET_WINDOW_VISIBILITY, windowId, hidden: false }; } +export function setFocusedWindow(window: WindowId): Dispatchable { + return { type: SET_FOCUSED_WINDOW, window }; +} + export function setWindowSize( windowId: WindowId, size: [number, number] @@ -104,7 +112,146 @@ export function toggleWindow(windowId: WindowId): Dispatchable { } export function updateWindowPositions( - positions: WindowPositions + positions: WindowPositions, + absolute?: boolean ): Dispatchable { - return { type: UPDATE_WINDOW_POSITIONS, positions }; + return { type: UPDATE_WINDOW_POSITIONS, positions, absolute }; +} + +export function centerWindowsInContainer(container: HTMLElement): Dispatchable { + return (dispatch, getState) => { + if (!Selectors.getPositionsAreRelative(getState())) { + return; + } + const { left, top } = container.getBoundingClientRect(); + const { scrollWidth: width, scrollHeight: height } = container; + dispatch(centerWindows({ left, top, width, height })); + }; +} + +export function centerWindowsInView(): Dispatchable { + return centerWindows({ + left: window.scrollX, + top: window.scrollY, + width: window.innerWidth, + height: window.innerHeight + }); +} + +export function centerWindows(box: { + left: number; + top: number; + width: number; + height: number; +}): Dispatchable { + return (dispatch, getState) => { + const state = getState(); + const windowsInfo = Selectors.getWindowsInfo(state); + const getOpen = Selectors.getWindowOpen(state); + const { top, left, width, height } = box; + + const offsetLeft = left + window.scrollX; + const offsetTop = top + window.scrollY; + + // A layout has been suplied. We will compute the bounding box and + // center the given layout. + const bounding = Utils.calculateBoundingBox( + windowsInfo.filter(w => getOpen(w.key)) + ); + + const boxHeight = bounding.bottom - bounding.top; + const boxWidth = bounding.right - bounding.left; + + const move = { + x: Math.ceil(offsetLeft - bounding.left + (width - boxWidth) / 2), + y: Math.ceil(offsetTop - bounding.top + (height - boxHeight) / 2) + }; + + const newPositions = windowsInfo.reduce( + (pos, w) => ({ + ...pos, + [w.key]: { x: move.x + w.x, y: move.y + w.y } + }), + {} + ); + + dispatch(updateWindowPositions(newPositions, true)); + }; +} + +export function browserWindowSizeChanged() { + const { height, width } = Utils.getWindowSize(); + return { type: BROWSER_WINDOW_SIZE_CHANGED, height, width }; +} + +export function resetWindowSizes(): Dispatchable { + return { type: RESET_WINDOW_SIZES }; +} + +export function stackWindows(): Dispatchable { + return (dispatch, getState) => { + dispatch( + updateWindowPositions(Selectors.getStackedLayoutPositions(getState())) + ); + }; +} + +export function ensureWindowsAreOnScreen(): Dispatchable { + return (dispatch, getState) => { + const state = getState(); + + const windowsInfo = Selectors.getWindowsInfo(state); + const getOpen = Selectors.getWindowOpen(state); + const { height, width } = Utils.getWindowSize(); + const bounding = Utils.calculateBoundingBox( + windowsInfo.filter(w => getOpen(w.key)) + ); + const positions = Selectors.getWindowPositions(state); + + // Are we good? + if ( + bounding.left >= 0 && + bounding.top >= 0 && + bounding.right <= width && + bounding.bottom <= height + ) { + // My work here is done. + return; + } + + const boundingHeight = bounding.bottom - bounding.top; + const boundingWidth = bounding.right - bounding.left; + + // Could we simply shift all the windows by a constant offset? + if (boundingWidth <= width && boundingHeight <= height) { + let moveY = 0; + let moveX = 0; + if (bounding.top <= 0) { + moveY = bounding.top; + } else if (bounding.bottom > height) { + moveY = bounding.bottom - height; + } + + if (bounding.left <= 0) { + moveX = bounding.left; + } else if (bounding.right > width) { + moveX = bounding.right - width; + } + + const newPositions = Utils.objectMap(positions, position => ({ + x: position.x - moveX, + y: position.y - moveY + })); + + dispatch(updateWindowPositions(newPositions)); + return; + } + + // TODO: Try moving the individual groups to try to fit them in + + // I give up. Just reset everything. + dispatch(resetWindowSizes()); + dispatch(stackWindows()); + dispatch(centerWindowsInView()); + }; } diff --git a/js/actionTypes.ts b/js/actionTypes.ts index ec1ce8f5..2267ba52 100644 --- a/js/actionTypes.ts +++ b/js/actionTypes.ts @@ -71,3 +71,6 @@ export const SET_DUMMY_VIZ_DATA = "SET_DUMMY_VIZ_DATA"; export const SET_WINDOW_VISIBILITY = "SET_WINDOW_VISIBILITY"; export const LOADING = "LOADING"; export const CLOSE_REQUESTED = "CLOSE_REQUESTED"; +export const LOAD_SERIALIZED_STATE = "LOAD_SERIALIZED_STATE"; +export const RESET_WINDOW_SIZES = "RESET_WINDOW_SIZES"; +export const BROWSER_WINDOW_SIZE_CHANGED = "BROWSER_WINDOW_SIZE_CHANGED"; diff --git a/js/components/App.js b/js/components/App.js index 7625cd5f..dc8e2f05 100644 --- a/js/components/App.js +++ b/js/components/App.js @@ -52,7 +52,7 @@ class App extends React.Component { _setFocus() { const binding = this._bindings[this.props.focused]; - if (binding.node) { + if (binding && binding.node) { binding.node.focus(); } } diff --git a/js/components/WindowManager.js b/js/components/WindowManager.js deleted file mode 100644 index a31e5816..00000000 --- a/js/components/WindowManager.js +++ /dev/null @@ -1,220 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; - -import { - snapDiffManyToMany, - boundingBox, - snapWithinDiff, - snap, - traceConnection, - applyDiff, - applyMultipleDiffs -} from "../snapUtils"; -import { getWindowsInfo, getWindowHidden, getWindowOpen } from "../selectors"; -import { updateWindowPositions } from "../actionCreators"; -import { WINDOW_HEIGHT, WINDOW_WIDTH } from "../constants"; -import { calculateBoundingBox } from "../utils"; - -const abuts = (a, b) => { - // TODO: This is kinda a hack. They should really be touching, not just within snapping distance. - // Also, overlapping should not count. - const wouldMoveTo = snap(a, b); - return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined; -}; - -class WindowManager extends React.Component { - componentDidMount() { - this.centerWindows(); - } - - centerWindows = () => { - const { container } = this.props; - - const rect = container.getBoundingClientRect(); - const offsetLeft = rect.left + window.scrollX; - const offsetTop = rect.top + window.scrollY; - const width = container.scrollWidth; - const height = container.scrollHeight; - - if (this.props.windowsInfo.some(w => w.x == null || w.y == null)) { - // Some windows do not have an initial position, so we'll come up - // with your own layout. - const windowPositions = {}; - const keys = this.windowKeys(); - const totalHeight = keys.length * WINDOW_HEIGHT; - const globalOffsetLeft = Math.max(0, width / 2 - WINDOW_WIDTH / 2); - const globalOffsetTop = Math.max(0, height / 2 - totalHeight / 2); - keys.forEach((key, i) => { - const offset = WINDOW_HEIGHT * i; - windowPositions[key] = { - x: Math.ceil(offsetLeft + globalOffsetLeft), - y: Math.ceil(offsetTop + (globalOffsetTop + offset)) - }; - }); - this.props.updateWindowPositions(windowPositions); - } else { - // A layout has been suplied. We will compute the bounding box and - // center the given layout. - const bounding = calculateBoundingBox( - this.props.windowsInfo.filter(w => this.props.getWindowOpen(w.key)) - ); - - const boxHeight = bounding.bottom - bounding.top; - const boxWidth = bounding.right - bounding.left; - - const move = { - x: Math.ceil(offsetLeft + (width - boxWidth) / 2), - y: Math.ceil(offsetTop + (height - boxHeight) / 2) - }; - - const newPositions = this.props.windowsInfo.reduce( - (pos, w) => ({ ...pos, [w.key]: { x: move.x + w.x, y: move.y + w.y } }), - {} - ); - - this.props.updateWindowPositions(newPositions); - } - }; - - movingAndStationaryNodes(key) { - const windows = this.props.windowsInfo.filter( - w => - this.props.windows[w.key] != null && !this.props.getWindowHidden(w.key) - ); - const targetNode = windows.find(node => node.key === key); - - let movingSet = new Set([targetNode]); - // Only the main window brings other windows along. - if (key === "main") { - const findAllConnected = traceConnection(abuts); - movingSet = findAllConnected(windows, targetNode); - } - - const stationary = windows.filter(w => !movingSet.has(w)); - const moving = Array.from(movingSet); - - return [moving, stationary]; - } - - handleMouseDown = (key, e) => { - if (!e.target.classList.contains("draggable")) { - return; - } - // Prevent dragging from highlighting text. - e.preventDefault(); - - const [moving, stationary] = this.movingAndStationaryNodes(key); - - const mouseStart = { x: e.clientX, y: e.clientY }; - // Aparently this is crazy across browsers. - const browserSize = { - width: Math.max( - document.body.scrollWidth, - document.documentElement.scrollWidth, - document.body.offsetWidth, - document.documentElement.offsetWidth, - document.body.clientWidth, - document.documentElement.clientWidth - ), - height: Math.max( - document.body.scrollHeight, - document.documentElement.scrollHeight, - document.body.offsetHeight, - document.documentElement.offsetHeight, - document.body.clientHeight, - document.documentElement.clientHeight - ) - }; - - const box = boundingBox(moving); - - const handleMouseMove = ee => { - const proposedDiff = { - x: ee.clientX - mouseStart.x, - y: ee.clientY - mouseStart.y - }; - - const proposedWindows = moving.map(node => ({ - ...node, - ...applyDiff(node, proposedDiff) - })); - - const proposedBox = { - ...box, - ...applyDiff(box, proposedDiff) - }; - - const snapDiff = snapDiffManyToMany(proposedWindows, stationary); - - const withinDiff = snapWithinDiff(proposedBox, browserSize); - - const finalDiff = applyMultipleDiffs(proposedDiff, snapDiff, withinDiff); - - const windowPositionDiff = moving.reduce((diff, window) => { - diff[window.key] = applyDiff(window, finalDiff); - return diff; - }, {}); - - this.props.updateWindowPositions(windowPositionDiff); - }; - - const removeListeners = () => { - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", removeListeners); - }; - - window.addEventListener("mouseup", removeListeners); - window.addEventListener("mousemove", handleMouseMove); - }; - - // Keys for the visible windows - windowKeys() { - // TODO: Iterables can probably do this better. - return Object.keys(this.props.windows).filter( - key => !!this.props.windows[key] - ); - } - - render() { - const style = { - position: "absolute", - top: 0, - left: 0 - }; - - const windows = this.props.windowsInfo.filter( - w => this.props.windows[w.key] - ); - - return windows.map(w => ( -
this.handleMouseDown(w.key, e)} - style={{ ...style, transform: `translate(${w.x}px, ${w.y}px)` }} - > - {this.props.windows[w.key]} -
- )); - } -} - -WindowManager.propTypes = { - windows: PropTypes.object.isRequired, - container: PropTypes.instanceOf(Element).isRequired -}; - -const mapStateToProps = state => ({ - windowsInfo: getWindowsInfo(state), - getWindowHidden: getWindowHidden(state), - getWindowOpen: getWindowOpen(state) -}); - -const mapDispatchToProps = { - updateWindowPositions -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(WindowManager); diff --git a/js/components/WindowManager.tsx b/js/components/WindowManager.tsx new file mode 100644 index 00000000..c6a95a32 --- /dev/null +++ b/js/components/WindowManager.tsx @@ -0,0 +1,161 @@ +import React, { ReactNode } from "react"; +import { connect } from "react-redux"; + +import { Box, Diff } from "../snapUtils"; +import * as SnapUtils from "../snapUtils"; +import * as Selectors from "../selectors"; +import { updateWindowPositions } from "../actionCreators"; +import { + WindowInfo, + Dispatch, + WindowPositions, + AppState, + WindowId +} from "../types"; +const abuts = (a: Box, b: Box) => { + // TODO: This is kinda a hack. They should really be touching, not just within snapping distance. + // Also, overlapping should not count. + const wouldMoveTo = SnapUtils.snap(a, b); + return wouldMoveTo.x !== undefined || wouldMoveTo.y !== undefined; +}; + +interface Props { + container: HTMLElement; + windowsInfo: WindowInfo[]; + browserWindowSize: { height: number; width: number }; + updateWindowPositions(positions: WindowPositions, center: boolean): void; + getWindowHidden(windowId: WindowId): boolean; + windows: { [windowId: string]: ReactNode }; +} + +class WindowManager extends React.Component { + movingAndStationaryNodes(key: WindowId): [WindowInfo[], WindowInfo[]] { + const windows = this.props.windowsInfo.filter( + w => + this.props.windows[w.key] != null && !this.props.getWindowHidden(w.key) + ); + const targetNode = windows.find(node => node.key === key); + if (targetNode == null) { + throw new Error("Tried to move a node that does not exist"); + } + + let movingSet = new Set([targetNode]); + // Only the main window brings other windows along. + if (key === "main") { + const findAllConnected = SnapUtils.traceConnection(abuts); + movingSet = findAllConnected(windows, targetNode); + } + + const stationary = windows.filter(w => !movingSet.has(w)); + const moving = Array.from(movingSet); + + return [moving, stationary]; + } + + handleMouseDown = (key: WindowId, e: React.MouseEvent) => { + if (!(e.target as HTMLElement).classList.contains("draggable")) { + return; + } + // Prevent dragging from highlighting text. + e.preventDefault(); + + const [moving, stationary] = this.movingAndStationaryNodes(key); + + const mouseStart = { x: e.clientX, y: e.clientY }; + const { browserWindowSize } = this.props; + + const box = SnapUtils.boundingBox(moving); + + const handleMouseMove = (ee: MouseEvent) => { + const proposedDiff = { + x: ee.clientX - mouseStart.x, + y: ee.clientY - mouseStart.y + }; + + const proposedWindows = moving.map(node => ({ + ...node, + ...SnapUtils.applyDiff(node, proposedDiff) + })); + + const proposedBox = { + ...box, + ...SnapUtils.applyDiff(box, proposedDiff) + }; + + const snapDiff = SnapUtils.snapDiffManyToMany( + proposedWindows, + stationary + ); + + const withinDiff = SnapUtils.snapWithinDiff( + proposedBox, + browserWindowSize + ); + + const finalDiff = SnapUtils.applyMultipleDiffs( + proposedDiff, + snapDiff, + withinDiff + ); + + const windowPositionDiff = moving.reduce( + (diff: { [windowId: string]: Diff }, window) => { + diff[window.key] = SnapUtils.applyDiff(window, finalDiff); + return diff; + }, + {} + ); + + this.props.updateWindowPositions(windowPositionDiff, false); + }; + + const removeListeners = () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", removeListeners); + }; + + window.addEventListener("mouseup", removeListeners); + window.addEventListener("mousemove", handleMouseMove); + }; + + render() { + const style: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0 + }; + + const windows = this.props.windowsInfo.filter( + w => this.props.windows[w.key] + ); + + return windows.map(w => ( +
this.handleMouseDown(w.key, e)} + style={{ ...style, transform: `translate(${w.x}px, ${w.y}px)` }} + > + {this.props.windows[w.key]} +
+ )); + } +} + +const mapStateToProps = (state: AppState) => ({ + windowsInfo: Selectors.getWindowsInfo(state), + getWindowHidden: Selectors.getWindowHidden(state), + getWindowOpen: Selectors.getWindowOpen(state), + browserWindowSize: Selectors.getBrowserWindowSize(state) +}); + +const mapDispatchToProps = (dispatch: Dispatch) => { + return { + updateWindowPositions: (positions: WindowPositions) => + dispatch(updateWindowPositions(positions)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(WindowManager); diff --git a/js/hotkeys.ts b/js/hotkeys.ts index 9b57251c..eef797c4 100644 --- a/js/hotkeys.ts +++ b/js/hotkeys.ts @@ -21,7 +21,7 @@ import { TOGGLE_TIME_MODE, TOGGLE_LLAMA_MODE } from "./actionTypes"; import { arraysAreEqual } from "./utils"; import { Dispatch } from "./types"; -export default function(dispatch: Dispatch) { +export function bindHotkeys(dispatch: Dispatch): () => void { let keylog: number[] = []; const trigger = [ 78, // N @@ -33,7 +33,8 @@ export default function(dispatch: Dispatch) { 70, // F 84 // T ]; - document.addEventListener("keydown", e => { + + const listener = (e: KeyboardEvent) => { if (e.ctrlKey) { switch (e.keyCode) { case 68: // CTRL+D @@ -142,5 +143,10 @@ export default function(dispatch: Dispatch) { dispatch({ type: TOGGLE_LLAMA_MODE }); } } - }); + }; + document.addEventListener("keydown", listener); + + return () => { + document.removeEventListener("keydown", listener); + }; } diff --git a/js/index.js b/js/index.js index 03cad884..ebe0540a 100644 --- a/js/index.js +++ b/js/index.js @@ -13,6 +13,7 @@ import green from "../skins/Green-Dimension-V2.wsz"; import internetArchive from "../skins/Internet-Archive.wsz"; import MilkdropWindow from "./components/MilkdropWindow"; import screenshotInitialState from "./screenshotInitialState"; + import WebampLazy from "./webampLazy"; import enableMediaSession from "./mediaSession"; import { @@ -28,6 +29,7 @@ import { SET_EQ_AUTO, SET_DUMMY_VIZ_DATA } from "./actionTypes"; + import { loadFilesFromReferences } from "./actionCreators"; import { @@ -37,6 +39,8 @@ import { disableMarquee } from "./config"; +import { bindToIndexedDB } from "./indexedDB"; + const requireJSZip = () => { return new Promise((resolve, reject) => { require.ensure( @@ -83,11 +87,15 @@ const NOISY_ACTION_TYPES = new Set([ const MIN_MILKDROP_WIDTH = 725; let screenshot = false; +let clearState = false; +let useState = false; let skinUrl = configSkinUrl; if ("URLSearchParams" in window) { const params = new URLSearchParams(location.search); screenshot = params.get("screenshot"); skinUrl = params.get("skinUrl") || skinUrl; + clearState = Boolean(params.get("clearState")); + useState = Boolean(params.get("useState")); } function supressDragAndDrop(e) { @@ -154,7 +162,7 @@ function genAudioFileUrlsFromDropbox() { }); } -Raven.context(() => { +Raven.context(async () => { window.Raven = Raven; if (screenshot) { document.getElementsByClassName("about")[0].style.visibility = "hidden"; @@ -283,6 +291,7 @@ Raven.context(() => { enableMediaSession(webamp); + // Expose a file input in the DOM for testing. const fileInput = document.createElement("input"); fileInput.id = "webamp-file-input"; fileInput.style.display = "none"; @@ -293,8 +302,10 @@ Raven.context(() => { }); document.body.appendChild(fileInput); - webamp.renderWhenReady(document.getElementById("app")); - // Expose webamp instance for debugging and integration tests. window.__webamp = webamp; + + await bindToIndexedDB(webamp, clearState, useState); + + await webamp.renderWhenReady(document.getElementById("app")); }); diff --git a/js/indexedDB.js b/js/indexedDB.js new file mode 100644 index 00000000..fd568ac9 --- /dev/null +++ b/js/indexedDB.js @@ -0,0 +1,41 @@ +import IdbKvStore from "idb-kv-store"; +import { throttle } from "./utils"; +const LOCAL_STORAGE_KEY = "webamp_state"; + +export async function bindToIndexedDB(webamp, clearState, useState) { + const localStore = new IdbKvStore("webamp_state_database"); + + if (clearState) { + try { + await localStore.clear(); + } catch (e) { + console.log("Failed to clear our IndexeddB state", e); + } + } + + if (!useState) { + return; + } + + let previousSerializedState = null; + try { + previousSerializedState = await localStore.get(LOCAL_STORAGE_KEY); + } catch (e) { + console.error("Failed to load the saved state from IndexedDB", e); + } + + if (previousSerializedState != null) { + webamp.__loadSerializedState(previousSerializedState); + } + + async function persist() { + const serializedState = webamp.__getSerializedState(); + try { + await localStore.set(LOCAL_STORAGE_KEY, serializedState); + } catch (e) { + console.log("Failed to save our state to IndexedDB", e); + } + } + + webamp.__onStateChange(throttle(persist, 1000)); +} diff --git a/js/mediaMiddleware.ts b/js/mediaMiddleware.ts index 7023c6b4..a2848790 100644 --- a/js/mediaMiddleware.ts +++ b/js/mediaMiddleware.ts @@ -17,11 +17,19 @@ import { SET_EQ_ON, PLAY_TRACK, BUFFER_TRACK, - CHANNEL_COUNT_CHANGED + CHANNEL_COUNT_CHANGED, + LOAD_SERIALIZED_STATE } from "./actionTypes"; import { next as nextTrack } from "./actionCreators"; -import { getCurrentTrackId } from "./selectors"; -import { MiddlewareStore, Dispatchable, Action, Dispatch } from "./types"; +import * as Selectors from "./selectors"; +import { + MiddlewareStore, + Dispatchable, + Action, + Dispatch, + Slider +} from "./types"; +import { objectForEach } from "./utils"; export default (media: Media) => (store: MiddlewareStore) => { const { @@ -58,7 +66,7 @@ export default (media: Media) => (store: MiddlewareStore) => { }); media.on("fileLoaded", () => { - const id = getCurrentTrackId(store.getState()); + const id = Selectors.getCurrentTrackId(store.getState()); if (id == null) { // Attempted to set the metadata for a track that was already removed. // Really, the media should have been stopped when the track was removed. @@ -82,7 +90,8 @@ export default (media: Media) => (store: MiddlewareStore) => { }); return (next: Dispatch) => (action: Action) => { - // TODO: Consider doing this after the action, and using the state as the source of truth. + const returnValue = next(action); + const state = store.getState(); switch (action.type) { case PLAY: media.play(); @@ -94,10 +103,10 @@ export default (media: Media) => (store: MiddlewareStore) => { media.stop(); break; case SET_VOLUME: - media.setVolume(action.volume); + media.setVolume(Selectors.getVolume(state)); break; case SET_BALANCE: - media.setBalance(action.balance); + media.setBalance(Selectors.getBalance(state)); break; case SEEK_TO_PERCENT_COMPLETE: media.seekToPercentComplete(action.percent); @@ -129,7 +138,27 @@ export default (media: Media) => (store: MiddlewareStore) => { case SET_EQ_ON: media.enableEq(); break; + case LOAD_SERIALIZED_STATE: { + // Set ALL THE THINGS! + if (Selectors.getEqualizerEnabled(state)) { + media.enableEq(); + } else { + media.disableEq(); + } + media.setVolume(Selectors.getVolume(state)); + media.setBalance(Selectors.getBalance(state)); + objectForEach(state.equalizer.sliders, (value, slider) => { + if (slider === "preamp") { + media.setPreamp(value); + } else { + // @ts-ignore I don't know how to teach TypeScript about objects + // that use Slider as keys + media.setEqBand(slider, value); + } + }); + break; + } } - return next(action); + return returnValue; }; }; diff --git a/js/reducers/display.ts b/js/reducers/display.ts index 7d3982d5..faad863c 100644 --- a/js/reducers/display.ts +++ b/js/reducers/display.ts @@ -23,9 +23,11 @@ import { SET_Z_INDEX, DISABLE_MARQUEE, SET_DUMMY_VIZ_DATA, - LOADING + LOADING, + LOAD_SERIALIZED_STATE } from "../actionTypes"; import { DEFAULT_SKIN, VISUALIZER_ORDER } from "../constants"; +import { DisplaySerializedStateV1 } from "../serializedStates/v1Types"; export interface DisplayState { additionalVisualizers: Array; @@ -123,12 +125,44 @@ const display = ( return { ...state, zIndex: action.zIndex }; case SET_DUMMY_VIZ_DATA: return { ...state, dummyVizData: action.data }; + case LOAD_SERIALIZED_STATE: + return { ...state, ...action.serializedState.display }; default: return state; } }; export default display; +export const getSerializedState = ( + state: DisplayState +): DisplaySerializedStateV1 => { + // My kingdom for a type-safe `_.pick`. + const { + visualizerStyle, + doubled, + llama, + marqueeStep, + skinImages, + skinCursors, + skinRegion, + skinGenLetterWidths, + skinColors, + skinPlaylistStyle + } = state; + return { + visualizerStyle, + doubled, + llama, + marqueeStep, + skinImages, + skinCursors, + skinRegion, + skinGenLetterWidths, + skinColors, + skinPlaylistStyle + }; +}; + export const getVisualizationOrder = (state: DisplayState): Array => { return [...state.additionalVisualizers, ...VISUALIZER_ORDER]; }; diff --git a/js/reducers/equalizer.ts b/js/reducers/equalizer.ts index ab8d08a9..1def5c6b 100644 --- a/js/reducers/equalizer.ts +++ b/js/reducers/equalizer.ts @@ -4,8 +4,10 @@ import { SET_BAND_VALUE, SET_EQ_AUTO, SET_EQ_ON, - SET_EQ_OFF + SET_EQ_OFF, + LOAD_SERIALIZED_STATE } from "../actionTypes"; +import { EqualizerSerializedStateV1 } from "../serializedStates/v1Types"; export interface EqualizerState { on: boolean; @@ -45,9 +47,17 @@ const equalizer = ( return { ...state, on: false }; case SET_EQ_AUTO: return { ...state, auto: action.value }; + case LOAD_SERIALIZED_STATE: + return action.serializedState.equalizer || state; default: return state; } }; +export function getSerializedState( + state: EqualizerState +): EqualizerSerializedStateV1 { + return state; +} + export default equalizer; diff --git a/js/reducers/media.ts b/js/reducers/media.ts index b9347c81..e128c480 100644 --- a/js/reducers/media.ts +++ b/js/reducers/media.ts @@ -13,9 +13,11 @@ import { TOGGLE_TIME_MODE, UPDATE_TIME_ELAPSED, ADD_TRACK_FROM_URL, - CHANNEL_COUNT_CHANGED + CHANNEL_COUNT_CHANGED, + LOAD_SERIALIZED_STATE } from "../actionTypes"; import { TIME_MODE, MEDIA_STATUS } from "../constants"; +import { MediaSerializedStateV1 } from "../serializedStates/v1Types"; export interface MediaState { timeMode: string; // TODO: Convert this to an enum @@ -98,9 +100,16 @@ const media = ( return { ...state, repeat: !state.repeat }; case TOGGLE_SHUFFLE: return { ...state, shuffle: !state.shuffle }; + case LOAD_SERIALIZED_STATE: + return { ...state, ...action.serializedState.media }; default: return state; } }; +export function getSerializedState(state: MediaState): MediaSerializedStateV1 { + const { volume, balance, shuffle, repeat } = state; + return { volume, balance, shuffle, repeat }; +} + export default media; diff --git a/js/reducers/playlist.ts b/js/reducers/playlist.ts index 8ad992c4..048c115b 100644 --- a/js/reducers/playlist.ts +++ b/js/reducers/playlist.ts @@ -28,9 +28,7 @@ import { shuffle, moveSelected, objectMap, objectFilter } from "../utils"; export interface PlaylistState { trackOrder: number[]; - // https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208 - // TODO: Using numbers for keys is kinda annoying. Consider retyping as string - tracks: { [id: number]: PlaylistTrack }; + tracks: { [id: string]: PlaylistTrack }; lastSelectedIndex: number | null; currentTrack: number | null; } diff --git a/js/reducers/windows.ts b/js/reducers/windows.ts index 9a1137f7..dbbb133d 100644 --- a/js/reducers/windows.ts +++ b/js/reducers/windows.ts @@ -1,4 +1,4 @@ -import { WebampWindow, WindowPositions, Action } from "../types"; +import { Action, WindowId } from "../types"; import { WINDOWS } from "../constants"; import { SET_FOCUSED_WINDOW, @@ -8,17 +8,54 @@ import { ADD_GEN_WINDOW, UPDATE_WINDOW_POSITIONS, WINDOW_SIZE_CHANGED, - TOGGLE_WINDOW_SHADE_MODE + TOGGLE_WINDOW_SHADE_MODE, + LOAD_SERIALIZED_STATE, + BROWSER_WINDOW_SIZE_CHANGED, + RESET_WINDOW_SIZES } from "../actionTypes"; +import * as Utils from "../utils"; +import { WindowsSerializedStateV1 } from "../serializedStates/v1Types"; -export interface WindowState { - focused: string; - genWindows: { [name: string]: WebampWindow }; - positions: WindowPositions; +export interface WindowPosition { + x: number; + y: number; } -const defaultWindowsState: WindowState = { +export type WindowPositions = { + [windowId: string]: WindowPosition; +}; + +export interface WebampWindow { + title: string; + size: [number, number]; + open: boolean; + hidden: boolean; + shade?: boolean; + canResize: boolean; + canShade: boolean; + canDouble: boolean; + generic: boolean; + hotkey?: string; + position: WindowPosition; +} + +export interface WindowInfo { + key: WindowId; + height: number; + width: number; + x: number; + y: number; +} +export interface WindowsState { + focused: string; + genWindows: { [name: string]: WebampWindow }; + browserWindowSize: { height: number; width: number }; + positionsAreRelative: boolean; +} + +const defaultWindowsState: WindowsState = { focused: WINDOWS.MAIN, + positionsAreRelative: true, genWindows: { // TODO: Remove static capabilites and derive them from ids/generic main: { @@ -31,7 +68,8 @@ const defaultWindowsState: WindowState = { canShade: true, canDouble: true, generic: false, - hotkey: "Alt+W" + hotkey: "Alt+W", + position: { x: 0, y: 0 } }, equalizer: { title: "Equalizer", @@ -43,7 +81,8 @@ const defaultWindowsState: WindowState = { canShade: true, canDouble: true, generic: false, - hotkey: "Alt+G" + hotkey: "Alt+G", + position: { x: 0, y: 0 } }, playlist: { title: "Playlist Editor", @@ -55,16 +94,17 @@ const defaultWindowsState: WindowState = { canShade: true, canDouble: false, generic: false, - hotkey: "Alt+E" + hotkey: "Alt+E", + position: { x: 0, y: 0 } } }, - positions: {} + browserWindowSize: { width: 0, height: 0 } }; const windows = ( - state: WindowState = defaultWindowsState, + state: WindowsState = defaultWindowsState, action: Action -): WindowState => { +): WindowsState => { switch (action.type) { case SET_FOCUSED_WINDOW: return { ...state, focused: action.window }; @@ -136,7 +176,8 @@ const windows = ( canShade: false, canResize: true, canDouble: false, - generic: true + generic: true, + position: { x: 0, y: 0 } } } }; @@ -160,11 +201,71 @@ const windows = ( case UPDATE_WINDOW_POSITIONS: return { ...state, - positions: { ...state.positions, ...action.positions } + positionsAreRelative: + action.absolute === true ? false : state.positionsAreRelative, + genWindows: Utils.objectMap(state.genWindows, (w, windowId) => { + const newPosition = action.positions[windowId]; + if (newPosition == null) { + return w; + } + return { ...w, position: newPosition }; + }) }; + case RESET_WINDOW_SIZES: + return { + ...state, + genWindows: Utils.objectMap(state.genWindows, w => ({ + ...w, + // Not sure why TypeScript can't figure this out for itself. + size: [0, 0] as [number, number] + })) + }; + case LOAD_SERIALIZED_STATE: { + const { + genWindows, + focused, + positionsAreRelative + } = action.serializedState.windows; + return { + ...state, + positionsAreRelative, + genWindows: Utils.objectMap(state.genWindows, (w, windowId) => { + const serializedW = genWindows[windowId]; + if (serializedW == null) { + return w; + } + return { ...w, ...serializedW }; + }), + focused + }; + } + case BROWSER_WINDOW_SIZE_CHANGED: + return { + ...state, + browserWindowSize: { height: action.height, width: action.width } + }; + default: return state; } }; +export function getSerializedState( + state: WindowsState +): WindowsSerializedStateV1 { + return { + positionsAreRelative: state.positionsAreRelative, + genWindows: Utils.objectMap(state.genWindows, w => { + return { + size: w.size, + open: w.open, + hidden: w.hidden, + shade: w.shade || false, + position: w.position + }; + }), + focused: state.focused + }; +} + export default windows; diff --git a/js/selectors.ts b/js/selectors.ts index 6bd12dca..bf68ada8 100644 --- a/js/selectors.ts +++ b/js/selectors.ts @@ -4,16 +4,11 @@ import { WebampWindow, WindowId, WindowInfo, - LoadedURLTrack + LoadedURLTrack, + WindowPositions } from "./types"; import { createSelector } from "reselect"; -import { - denormalize, - getTimeStr, - clamp, - percentToIndex, - objectMap -} from "./utils"; +import * as Utils from "./utils"; import { BANDS, TRACK_HEIGHT, @@ -26,17 +21,21 @@ import { import { createPlaylistURL } from "./playlistHtml"; import * as fromPlaylist from "./reducers/playlist"; import * as fromDisplay from "./reducers/display"; +import * as fromEqualizer from "./reducers/equalizer"; +import * as fromMedia from "./reducers/media"; +import * as fromWindows from "./reducers/windows"; import { generateGraph } from "./resizeUtils"; +import { SerializedStateV1 } from "./serializedStates/v1Types"; export const getSliders = (state: AppState) => state.equalizer.sliders; export const getEqfData = createSelector(getSliders, sliders => { const preset: { [key: string]: number | string } = { name: "Entry1", - preamp: denormalize(sliders.preamp) + preamp: Utils.denormalize(sliders.preamp) }; BANDS.forEach(band => { - preset[`hz${band}`] = denormalize(sliders[band]); + preset[`hz${band}`] = Utils.denormalize(sliders[band]); }); const eqfData = { presets: [preset], @@ -89,7 +88,9 @@ export const getRunningTimeMessage = createSelector( getTotalRunningTime, getSelectedRunningTime, (totalRunningTime, selectedRunningTime) => - `${getTimeStr(selectedRunningTime)}/${getTimeStr(totalRunningTime)}` + `${Utils.getTimeStr(selectedRunningTime)}/${Utils.getTimeStr( + totalRunningTime + )}` ); // TODO: use slectors to get memoization @@ -137,7 +138,7 @@ export const nextTrack = (state: AppState, n = 1) => { return null; } - nextIndex = clamp(nextIndex, 0, trackCount - 1); + nextIndex = Utils.clamp(nextIndex, 0, trackCount - 1); return trackOrder[nextIndex]; }; @@ -181,7 +182,7 @@ export const getScrollOffset = createSelector( getNumberOfVisibleTracks, (playlistScrollPosition, trackCount, numberOfVisibleTracks) => { const overflow = Math.max(0, trackCount - numberOfVisibleTracks); - return percentToIndex(playlistScrollPosition / 100, overflow + 1); + return Utils.percentToIndex(playlistScrollPosition / 100, overflow + 1); } ); @@ -287,7 +288,7 @@ export const getMediaText = createSelector( minimalMediaText == null ? null : // TODO: Maybe the ` *** ` should actually be added by the marquee - `${minimalMediaText} (${getTimeStr(duration)}) *** ` + `${minimalMediaText} (${Utils.getTimeStr(duration)}) *** ` ); export const getNumberOfTracks = (state: AppState) => @@ -308,21 +309,19 @@ export const getPlaylistURL = createSelector( (numberOfTracks, playlistDuration, trackOrder, tracks, getDisplayName) => createPlaylistURL({ numberOfTracks, - averageTrackLength: getTimeStr(playlistDuration / numberOfTracks), + averageTrackLength: Utils.getTimeStr(playlistDuration / numberOfTracks), // TODO: Handle hours playlistLengthMinutes: Math.floor(playlistDuration / 60), playlistLengthSeconds: Math.floor(playlistDuration % 60), tracks: trackOrder.map( (id, i) => - `${i + 1}. ${getDisplayName(id)} (${getTimeStr(tracks[id].duration)})` + `${i + 1}. ${getDisplayName(id)} (${Utils.getTimeStr( + tracks[id].duration + )})` ) }) ); -export function getWindowPositions(state: AppState) { - return state.windows.positions; -} - const WINDOW_HEIGHT = 116; const SHADE_WINDOW_HEIGHT = 14; @@ -355,19 +354,40 @@ export function getWindowHidden(state: AppState) { return (windowId: WindowId) => state.windows.genWindows[windowId].hidden; } +export function getFocusedWindow(state: AppState): WindowId { + return state.windows.focused; +} + +export function getWindowPosition(state: AppState) { + return (windowId: WindowId) => state.windows.genWindows[windowId].position; +} + +export function getPositionsAreRelative(state: AppState) { + return state.windows.positionsAreRelative; +} + export const getGenWindows = (state: AppState) => { return state.windows.genWindows; }; +export const getWindowPositions = createSelector( + getGenWindows, + (windows): WindowPositions => Utils.objectMap(windows, w => w.position) +); + export function getDoubled(state: AppState) { return state.display.doubled; } +export function getLlamaMode(state: AppState) { + return state.display.llama; +} + export const getWindowSizes = createSelector( getGenWindows, getDoubled, (windows, doubled) => { - return objectMap(windows, w => getWPixelSize(w, doubled)); + return Utils.objectMap(windows, w => getWPixelSize(w, doubled)); } ); @@ -375,6 +395,7 @@ export const getWindowPixelSize = createSelector(getWindowSizes, sizes => { return (windowId: WindowId) => sizes[windowId]; }); +// TODO: Now that both size and position are stored on genWindows this seems a bit silly. export const getWindowsInfo = createSelector( getWindowSizes, getWindowPositions, @@ -401,5 +422,47 @@ export const getVisualizerStyle = (state: AppState) => export const getVolume = (state: AppState) => state.media.volume; export const getBalance = (state: AppState) => state.media.balance; +export const getShuffle = (state: AppState) => state.media.shuffle; +export const getRepeat = (state: AppState) => state.media.repeat; export const getChannels = (state: AppState) => state.media.channels; +export function getSerlializedState(state: AppState): SerializedStateV1 { + return { + version: 1, + media: fromMedia.getSerializedState(state.media), + equalizer: fromEqualizer.getSerializedState(state.equalizer), + display: fromDisplay.getSerializedState(state.display), + windows: fromWindows.getSerializedState(state.windows) + }; +} + +export function getEqualizerEnabled(state: AppState): boolean { + return state.equalizer.on; +} + +export function getEqualizerAuto(state: AppState): boolean { + return state.equalizer.auto; +} + +export function getBrowserWindowSize( + state: AppState +): { height: number; width: number } { + return state.windows.browserWindowSize; +} + +export const getOpenWindows = createSelector(getGenWindows, genWindows => + Utils.objectFilter(genWindows, w => w.open) +); + +export const getStackedLayoutPositions = createSelector( + getOpenWindows, + getDoubled, + (openWindows, doubled): WindowPositions => { + let offset = 0; + return Utils.objectMap(openWindows, w => { + const position = { x: 0, y: offset }; + offset += getWPixelSize(w, doubled).height; + return position; + }); + } +); diff --git a/js/serialization.test.ts b/js/serialization.test.ts new file mode 100644 index 00000000..f9355f66 --- /dev/null +++ b/js/serialization.test.ts @@ -0,0 +1,239 @@ +import fs from "fs"; +import path from "path"; +import reducer from "./reducers"; +import * as Selectors from "./selectors"; +import * as Actions from "./actionCreators"; +import { kebabCase } from "lodash"; + +import { createStore, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; +import { LOAD_SERIALIZED_STATE } from "./actionTypes"; +import { SerializedStateV1 } from "./serializedStates/v1Types"; +import { Dispatchable, AppState } from "./types"; + +function fixturePath(name: string) { + return path.join( + __dirname, + "__tests__/fixtures/serializedState/v1/", + `${kebabCase(name)}.json` + ); +} + +function writeFixture(name: string, state: SerializedStateV1) { + fs.writeFileSync(fixturePath(name), JSON.stringify(state, null, 4)); +} + +function readFixture(name: string): SerializedStateV1 { + return JSON.parse(fs.readFileSync(fixturePath(name), "utf8")); +} + +interface SerializationTestParams { + name: string; + action: Dispatchable; + selector(state: AppState): T; + expected: T; +} + +function testSerialization({ + name, + action, + selector, + expected +}: SerializationTestParams) { + test(name, () => { + const firstStore = getStore(); + // Does not match the expected selection in the initial state + expect(selector(firstStore.getState())).not.toEqual(expected); + + firstStore.dispatch(action); + // Ensure we actually changed something + expect(firstStore.getState()).not.toEqual(getStore().getState()); + + // Ensure the expectation is true before we serialize + expect(selector(firstStore.getState())).toEqual(expected); + + const serializedState = Selectors.getSerlializedState( + firstStore.getState() + ); + + if (!fs.existsSync(fixturePath(name))) { + writeFixture(name, serializedState); + console.warn("Wrote new fixture: ", fixturePath(name)); + } + + const readSerializedState = readFixture(name); + + // Ensure our serialization strategy hasn't changed since we last serialized + expect(readSerializedState).toEqual(serializedState); + + // Try to apply this serialized state to the default state + const secondStore = getStore(); + secondStore.dispatch({ + type: LOAD_SERIALIZED_STATE, + serializedState: readSerializedState + }); + + // Ensure our restored state conforms to expectation + expect(selector(secondStore.getState())).toEqual(expected); + + // TODO: Try to apply the serialized state to a complex non-initial state + }); +} + +const defaultSerializedState = Selectors.getSerlializedState( + getStore().getState() +); + +function getStore() { + const extras = {}; + const enhancer = applyMiddleware(thunk.withExtraArgument(extras)); + return createStore(reducer, enhancer); +} + +describe("can serialize", () => { + test("the initial state", () => { + const store = getStore(); + const serializedState = Selectors.getSerlializedState(store.getState()); + expect(serializedState).toMatchSnapshot(); + + const newStore = getStore(); + newStore.dispatch({ + type: LOAD_SERIALIZED_STATE, + serializedState + }); + expect(store.getState()).toEqual(newStore.getState()); + }); + + /* Media */ + testSerialization({ + name: "volume", + action: Actions.setVolume(50), + selector: Selectors.getVolume, + expected: 50 + }); + + testSerialization({ + name: "balance", + action: Actions.setBalance(40), + selector: Selectors.getBalance, + expected: 40 + }); + + testSerialization({ + name: "shuffle", + action: Actions.toggleShuffle(), + selector: Selectors.getShuffle, + expected: true + }); + + testSerialization({ + name: "repeat", + action: Actions.toggleRepeat(), + selector: Selectors.getRepeat, + expected: true + }); + + /* Equalizer */ + testSerialization({ + name: "equalizer on", + action: Actions.toggleEq(), + selector: Selectors.getEqualizerEnabled, + expected: false + }); + + testSerialization({ + name: "equalizer auto", + action: Actions.toggleEqAuto(), + selector: Selectors.getEqualizerAuto, + expected: true + }); + + testSerialization({ + name: "equalizer band", + action: Actions.setEqBand(60, 100), + selector: state => Selectors.getSliders(state)[60], + expected: 100 + }); + + testSerialization({ + name: "equalizer preamp", + action: Actions.setPreamp(10), + selector: state => Selectors.getSliders(state).preamp, + expected: 10 + }); + + /* Display */ + testSerialization({ + name: "double mode", + action: Actions.toggleDoubleSizeMode(), + selector: Selectors.getDoubled, + expected: true + }); + + testSerialization({ + name: "llama mode", + action: Actions.toggleLlamaMode(), + selector: Selectors.getLlamaMode, + expected: true + }); + + // TODO + // * marqueeStep + // * skinImages + // * skinCursors + // * skinRegion + // * skinGenLetterWidths + // * skinColors + // * skinPlaylistStyle + // * focused + + /* Gen Windows */ + testSerialization({ + name: "window size", + action: Actions.setWindowSize("playlist", [4, 5]), + selector: state => Selectors.getWindowSize(state)("playlist"), + expected: [4, 5] + }); + + testSerialization({ + name: "window open", + action: Actions.closeWindow("playlist"), + selector: state => Selectors.getWindowOpen(state)("playlist"), + expected: false + }); + + testSerialization({ + name: "window hidden", + action: Actions.hideWindow("playlist"), + selector: state => Selectors.getWindowHidden(state)("playlist"), + expected: true + }); + + testSerialization({ + name: "window shade", + action: Actions.toggleEqualizerShadeMode(), + selector: state => Selectors.getWindowShade(state)("equalizer"), + expected: true + }); + + testSerialization({ + name: "main window position", + action: Actions.updateWindowPositions({ main: { x: 100, y: 100 } }), + selector: state => Selectors.getWindowPosition(state)("main"), + expected: { x: 100, y: 100 } + }); + + testSerialization({ + name: "window positions are relative", + action: Actions.updateWindowPositions({ main: { x: 100, y: 100 } }, true), + selector: Selectors.getPositionsAreRelative, + expected: false + }); + + testSerialization({ + name: "focused window", + action: Actions.setFocusedWindow("equalizer"), + selector: state => Selectors.getFocusedWindow(state), + expected: "equalizer" + }); +}); diff --git a/js/serializedStates/v1Types.ts b/js/serializedStates/v1Types.ts new file mode 100644 index 00000000..533f988e --- /dev/null +++ b/js/serializedStates/v1Types.ts @@ -0,0 +1,64 @@ +/** + * NOTE: This file must not import any other types + */ + +export interface WindowsSerializedStateV1 { + positionsAreRelative: boolean; + genWindows: { + [windowId: string]: { + size: [number, number]; + open: boolean; + hidden: boolean; + shade: boolean; + position: { x: number; y: number }; + }; + }; + focused: string; +} + +export interface DisplaySerializedStateV1 { + visualizerStyle: number; + doubled: boolean; + llama: boolean; + marqueeStep: number; + skinImages: { [sprite: string]: string }; + skinCursors: { [cursor: string]: string } | null; + skinRegion: { [windowName: string]: string[] }; + skinGenLetterWidths: { [letter: string]: number } | null; + skinColors: string[]; // Theoretically this could be a tuple of a specific length + skinPlaylistStyle: { [state: string]: string } | null; +} + +export interface EqualizerSerializedStateV1 { + on: boolean; + auto: boolean; + sliders: Record< + | 60 + | 170 + | 310 + | 600 + | 1000 + | 3000 + | 6000 + | 12000 + | 14000 + | 16000 + | "preamp", + number + >; +} + +export interface MediaSerializedStateV1 { + volume: number; + balance: number; + shuffle: boolean; + repeat: boolean; +} + +export interface SerializedStateV1 { + version: 1; + windows: WindowsSerializedStateV1; + media: MediaSerializedStateV1; + display: DisplaySerializedStateV1; + equalizer: EqualizerSerializedStateV1; +} diff --git a/js/snapUtils.ts b/js/snapUtils.ts index e78147e9..99eb1eb8 100644 --- a/js/snapUtils.ts +++ b/js/snapUtils.ts @@ -3,7 +3,7 @@ interface Point { y: number; } -interface Diff { +export interface Diff { x?: number; y?: number; } @@ -13,7 +13,7 @@ interface BoundingBox { height: number; } -interface Box extends Point { +export interface Box extends Point { width: number; height: number; } @@ -64,7 +64,7 @@ export const snap = (boxA: Box, boxB: Box) => { return { x, y }; }; -export const snapDiff = (a: Box, b: Box): Diff => { +export const snapDiff = (a: Box, b: Box): Point => { const newPos = snap(a, b); return { x: newPos.x === undefined ? 0 : newPos.x - a.x, @@ -73,7 +73,7 @@ export const snapDiff = (a: Box, b: Box): Diff => { }; // TODO: Use the first x and y combo -export const snapDiffManyToMany = (as: Box[], bs: Box[]): Diff => { +export const snapDiffManyToMany = (as: Box[], bs: Box[]): Point => { let x: number | undefined = 0; let y: number | undefined = 0; for (const a of as) { @@ -120,7 +120,7 @@ export const snapWithin = (boxA: Box, boundingBox: BoundingBox): Diff => { return { x, y }; }; -export const snapWithinDiff = (a: Box, b: Box) => { +export const snapWithinDiff = (a: Box, b: BoundingBox) => { const newPos = snapWithin(a, b); return { x: newPos.x === undefined ? 0 : newPos.x - a.x, diff --git a/js/types.ts b/js/types.ts index a8b21c84..75565330 100644 --- a/js/types.ts +++ b/js/types.ts @@ -3,9 +3,17 @@ import { SettingsState } from "./reducers/settings"; import { UserInputState } from "./reducers/userInput"; import { MediaState } from "./reducers/media"; import { DisplayState } from "./reducers/display"; -import { WindowState } from "./reducers/windows"; +import { WindowsState, WindowPositions } from "./reducers/windows"; import { EqualizerState } from "./reducers/equalizer"; import { NetworkState } from "./reducers/network"; +import { SerializedStateV1 } from "./serializedStates/v1Types"; + +export { + WebampWindow, + WindowInfo, + WindowPosition, + WindowPositions +} from "./reducers/windows"; export type Skin = { url: string; @@ -53,13 +61,6 @@ type SkinData = { skinGenLetterWidths: GenLetterWidths; }; -interface WindowPosition { - x: number; - y: number; -} -export type WindowPositions = { - [windowId: string]: WindowPosition; -}; export type Action = | { type: "@@init"; @@ -253,6 +254,7 @@ export type Action = | { type: "UPDATE_WINDOW_POSITIONS"; positions: WindowPositions; + absolute?: boolean; } | { type: "CLICKED_TRACK"; @@ -341,28 +343,13 @@ export type Action = | { type: "CLOSE_REQUESTED"; cancel: () => void; - }; - -export interface WebampWindow { - title: string; - size: [number, number]; - open: boolean; - hidden: boolean; - shade?: boolean; - canResize: boolean; - canShade: boolean; - canDouble: boolean; - generic: boolean; - hotkey?: string; -} - -export interface WindowInfo { - key: WindowId; - height: number; - width: number; - x: number; - y: number; -} + } + | { + type: "LOAD_SERIALIZED_STATE"; + serializedState: SerializedStateV1; + } + | { type: "RESET_WINDOW_SIZES" } + | { type: "BROWSER_WINDOW_SIZE_CHANGED"; height: number; width: number }; export type MediaTagRequestStatus = | "INITIALIZED" @@ -457,7 +444,7 @@ export interface PlaylistTrack { export interface AppState { userInput: UserInputState; - windows: WindowState; + windows: WindowsState; display: DisplayState; settings: SettingsState; equalizer: EqualizerState; diff --git a/js/utils.ts b/js/utils.ts index 5e4ceb39..4231e0a5 100644 --- a/js/utils.ts +++ b/js/utils.ts @@ -1,4 +1,5 @@ import { DEFAULT_SKIN } from "./constants"; +import { WindowInfo } from "./types"; interface Time { minutesFirstDigit: string; @@ -254,18 +255,37 @@ export function spliceIn(original: T[], start: number, newValues: T[]): T[] { return newArr; } -type Procedure = (...args: any[]) => void; +export function debounce(func: Function, delay: number): Function { + let timeout: number; + let callbackArgs: any[] = []; -export function debounce(func: F, delay: number): F { - let token: NodeJS.Timer; - return function(this: any, ...args: any[]): void { - if (token != null) { - clearTimeout(token); + return function(context: Object, ...args: any[]): void { + callbackArgs = args; + + if (timeout != null) { + clearTimeout(timeout); } - token = setTimeout(() => { - func.apply(this, args); + timeout = window.setTimeout(() => { + func.apply(context, callbackArgs); }, delay); - } as any; + }; +} + +// Trailing edge only throttle +export function throttle(func: Function, delay: number): Function { + let timeout: number | null = null; + let callbackArgs: any[] = []; + + return function(context: Object, ...args: any[]): void { + callbackArgs = args; + + if (!timeout) { + timeout = window.setTimeout(() => { + func.apply(context, callbackArgs); + timeout = null; + }, delay); + } + }; } let counter = 0; @@ -292,7 +312,7 @@ export function objectMap( export function objectFilter( obj: { [key: string]: V }, predicate: (value: V, key: string) => boolean -) { +): { [key: string]: V } { // TODO: Could return the original reference if no values change return Object.keys(obj).reduce((newObj: { [key: string]: V }, key) => { if (predicate(obj[key], key)) { @@ -302,25 +322,20 @@ export function objectFilter( }, {}); } -interface Window { - left: number; - top: number; - x: number; - y: number; - width: number; - height: number; -} - -export const calculateBoundingBox = (windows: Window[]) => - windows.reduce( - (b, w) => ({ - left: Math.min(b.left, w.x), - top: Math.min(b.top, w.y), - bottom: Math.max(b.bottom, w.y + w.height), - right: Math.max(b.right, w.x + w.width) - }), - { top: 0, bottom: 0, left: 0, right: 0 } - ); +export const calculateBoundingBox = (windows: WindowInfo[]) => + windows + .map(w => ({ + left: w.x, + top: w.y, + bottom: w.y + w.height, + right: w.x + w.width + })) + .reduce((b, w) => ({ + left: Math.min(b.left, w.left), + top: Math.min(b.top, w.top), + bottom: Math.max(b.bottom, w.bottom), + right: Math.max(b.right, w.right) + })); export function findLastIndex(arr: T[], cb: (val: T) => boolean) { for (let i = arr.length - 1; i >= 0; i--) { @@ -330,3 +345,25 @@ export function findLastIndex(arr: T[], cb: (val: T) => boolean) { } return -1; } + +export function getWindowSize(): { width: number; height: number } { + // Aparently this is crazy across browsers. + return { + width: Math.max( + document.body.scrollWidth, + document.documentElement.scrollWidth, + document.body.offsetWidth, + document.documentElement.offsetWidth, + document.body.clientWidth, + document.documentElement.clientWidth + ), + height: Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight, + document.body.offsetHeight, + document.documentElement.offsetHeight, + document.body.clientHeight, + document.documentElement.clientHeight + ) + }; +} diff --git a/js/webampLazy.js b/js/webampLazy.js index 87042525..26376679 100644 --- a/js/webampLazy.js +++ b/js/webampLazy.js @@ -4,22 +4,13 @@ import { Provider } from "react-redux"; import getStore from "./store"; import App from "./components/App"; -import Hotkeys from "./hotkeys"; +import { bindHotkeys } from "./hotkeys"; import Media from "./media"; import * as Selectors from "./selectors"; -import { - setSkinFromUrl, - loadMediaFiles, - setWindowSize, - play, - pause, - seekBackward, - seekForward, - next, - previous -} from "./actionCreators"; +import * as Actions from "./actionCreators"; + import { LOAD_STYLE } from "./constants"; -import { uniqueId, objectMap, objectForEach } from "./utils"; +import * as Utils from "./utils"; import { SET_AVAILABLE_SKINS, @@ -28,7 +19,6 @@ import { CLOSE_WINAMP, MINIMIZE_WINAMP, ADD_GEN_WINDOW, - UPDATE_WINDOW_POSITIONS, LOADED, REGISTER_VISUALIZER, SET_Z_INDEX, @@ -64,6 +54,7 @@ class Winamp { } constructor(options) { + this._subscriptions = []; this._actionEmitter = new Emitter(); this.options = options; const { @@ -99,7 +90,7 @@ class Winamp { this.genWindows = []; if (__extraWindows) { this.genWindows = __extraWindows.map(genWindow => ({ - id: genWindow.id || `${genWindow.title}-${uniqueId()}`, + id: genWindow.id || `${genWindow.title}-${Utils.uniqueId()}`, ...genWindow })); @@ -126,8 +117,14 @@ class Winamp { this.store.dispatch({ type: NETWORK_DISCONNECTED }) ); + this.store.dispatch(Actions.browserWindowSizeChanged()); + window.addEventListener("resize", () => { + this.store.dispatch(Actions.browserWindowSizeChanged()); + this.store.dispatch(Actions.ensureWindowsAreOnScreen()); + }); + if (initialSkin) { - this.store.dispatch(setSkinFromUrl(initialSkin.url)); + this.store.dispatch(Actions.setSkinFromUrl(initialSkin.url)); } else { // We are using the default skin. this.store.dispatch({ type: LOADED }); @@ -147,56 +144,62 @@ class Winamp { } const layout = options.__initialWindowLayout; - if (layout != null) { - objectForEach(layout, (w, windowId) => { + if (layout == null) { + this.store.dispatch(Actions.stackWindows()); + } else { + Utils.objectForEach(layout, (w, windowId) => { if (w.size != null) { - this.store.dispatch(setWindowSize(windowId, w.size)); + this.store.dispatch(Actions.setWindowSize(windowId, w.size)); } }); - this.store.dispatch({ - type: UPDATE_WINDOW_POSITIONS, - positions: objectMap(layout, w => w.position) - }); + this.store.dispatch( + Actions.updateWindowPositions( + Utils.objectMap(layout, w => w.position), + true + ) + ); } if (enableHotkeys) { - new Hotkeys(this.store.dispatch); + this._subscriptions.push(bindHotkeys(this.store.dispatch)); } } play() { - this.store.dispatch(play()); + this.store.dispatch(Actions.play()); } pause() { - this.store.dispatch(pause()); + this.store.dispatch(Actions.pause()); } seekBackward(seconds) { - this.store.dispatch(seekBackward(seconds)); + this.store.dispatch(Actions.seekBackward(seconds)); } seekForward(seconds) { - this.store.dispatch(seekForward(seconds)); + this.store.dispatch(Actions.seekForward(seconds)); } nextTrack() { - this.store.dispatch(next()); + this.store.dispatch(Actions.next()); } previousTrack() { - this.store.dispatch(previous()); + this.store.dispatch(Actions.previous()); } // Append this array of tracks to the end of the current playlist. appendTracks(tracks) { const nextIndex = Selectors.getTrackCount(this.store.getState()); - this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex)); + this.store.dispatch( + Actions.loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex) + ); } // Replace any existing tracks with this array of tracks, and begin playing. setTracksToPlay(tracks) { - this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY)); + this.store.dispatch(Actions.loadMediaFiles(tracks, LOAD_STYLE.PLAY)); } onWillClose(cb) { @@ -231,7 +234,20 @@ class Winamp { return storeHas(this.store, state => !state.display.loading); } + __loadSerializedState(serializedState) { + this.store.dispatch(Actions.loadSerializedState(serializedState)); + } + + __getSerializedState() { + return Selectors.getSerlializedState(this.store.getState()); + } + + __onStateChange(cb) { + return this.store.subscribe(cb); + } + async renderWhenReady(node) { + this.store.dispatch(Actions.centerWindowsInContainer(node)); await this.skinIsLoaded(); const genWindowComponents = {}; this.genWindows.forEach(w => { diff --git a/package.json b/package.json index acabe5f9..303e341c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/classnames": "^2.2.6", "@types/invariant": "^2.2.29", "@types/jest": "^23.3.2", + "@types/lodash": "^4.14.116", "@types/react": "^16.4.14", "@types/react-dom": "^16.0.7", "@types/react-redux": "^6.0.8", @@ -89,6 +90,7 @@ "html-webpack-inline-svg-plugin": "^1.2.4", "html-webpack-plugin": "^3.0.6", "http-server": "^0.11.1", + "idb-kv-store": "^4.4.0", "invariant": "^2.2.3", "jest": "^23.5.0", "jest-image-snapshot": "^2.4.1", @@ -97,6 +99,7 @@ "jest-runner-eslint": "^0.4.0", "jsmediatags": "^3.8.1", "jszip": "^3.1.3", + "lodash": "^4.17.11", "milkdrop-preset-converter-aws": "^0.1.0", "prettier": "^1.14.2", "prop-types": "^15.5.10", diff --git a/redux.md b/redux.md new file mode 100644 index 00000000..22339534 --- /dev/null +++ b/redux.md @@ -0,0 +1,19 @@ +# Thoughts on Redux + +## Actions describe a thing that happened + +Actions with names like `"SET_USER_NAME"` are a smell. Instead, prefer names like `"USERNAME_INPUT_CHANGED"`. Your actions should be a log of _facts_ and should not have any opinions about how those facts are interperated. + +## Actions and action creators are a global concern + +Reduces should know about actions, but actions should not know about your reducers. + +## Your state is a cache + +In principle, your state could simply be an array of every action that has been dispatched, and you could derive the current value by running your reducer in your selector. + +### What would this redux look like? How could you optimize it? + +- Often you need to refer to some other portion of your state within your reducer. How would you do this? + +## diff --git a/yarn.lock b/yarn.lock index 220c3ca2..4f1d7278 100644 --- a/yarn.lock +++ b/yarn.lock @@ -733,6 +733,10 @@ version "23.3.2" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.2.tgz#07b90f6adf75d42c34230c026a2529e56c249dbb" +"@types/lodash@^4.14.116": + version "4.14.116" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9" + "@types/node@*": version "10.1.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.0.tgz#2783ee1b6c47cbd4044f4a233976c1ac5fa9e942" @@ -4150,6 +4154,13 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" +idb-kv-store@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/idb-kv-store/-/idb-kv-store-4.4.0.tgz#22c56a8d5f90bd88f818a859db9c58627de278be" + dependencies: + inherits "^2.0.3" + promisize "^1.1.2" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -5402,6 +5413,10 @@ lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" +lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" @@ -6720,6 +6735,10 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +promisize@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/promisize/-/promisize-1.1.2.tgz#9b47e2cb2ae497eb1ebadc2c4191d64d15c949d1" + prompts@^0.1.9: version "0.1.14" resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"