Revert "Revert "Serialize to indexdb" (#664)"

This reverts commit a053818c5e.
This commit is contained in:
Jordan Eldredge 2018-10-02 22:00:21 -07:00
parent 4f48ebb89e
commit fc84ad4abf
45 changed files with 3207 additions and 392 deletions

View file

@ -4,6 +4,9 @@
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
/* Prevent accidental highlighting */

View file

@ -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,
},
}
`;

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

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

View file

@ -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());
};
}

View file

@ -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());
};
}

View file

@ -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";

View file

@ -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();
}
}

View file

@ -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 => (
<div
key={w.key}
onMouseDown={e => this.handleMouseDown(w.key, e)}
style={{ ...style, transform: `translate(${w.x}px, ${w.y}px)` }}
>
{this.props.windows[w.key]}
</div>
));
}
}
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);

View file

@ -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<Props> {
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<HTMLDivElement>) => {
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 => (
<div
key={w.key}
onMouseDown={e => this.handleMouseDown(w.key, e)}
style={{ ...style, transform: `translate(${w.x}px, ${w.y}px)` }}
>
{this.props.windows[w.key]}
</div>
));
}
}
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);

View file

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

View file

@ -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"));
});

41
js/indexedDB.js Normal file
View file

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

View file

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

View file

@ -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<string>;
@ -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<string> => {
return [...state.additionalVisualizers, ...VISUALIZER_ORDER];
};

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -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;

View file

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

239
js/serialization.test.ts Normal file
View file

@ -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<T> {
name: string;
action: Dispatchable;
selector(state: AppState): T;
expected: T;
}
function testSerialization<T>({
name,
action,
selector,
expected
}: SerializationTestParams<T>) {
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"
});
});

View file

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

View file

@ -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,

View file

@ -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;

View file

@ -1,4 +1,5 @@
import { DEFAULT_SKIN } from "./constants";
import { WindowInfo } from "./types";
interface Time {
minutesFirstDigit: string;
@ -254,18 +255,37 @@ export function spliceIn<T>(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<F extends Procedure>(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<V, N>(
export function objectFilter<V>(
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<V>(
}, {});
}
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<T>(arr: T[], cb: (val: T) => boolean) {
for (let i = arr.length - 1; i >= 0; i--) {
@ -330,3 +345,25 @@ export function findLastIndex<T>(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
)
};
}

View file

@ -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 => {

View file

@ -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",

19
redux.md Normal file
View file

@ -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?
##

View file

@ -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"