Browse Source

Rework webplayer

Full rework of webplayer. Webplayer is back to its previous working
state, and ready for further improvements.
Phyks (Lucas Verney) 5 years ago
parent
commit
d8a7d4f66a
71 changed files with 776 additions and 431 deletions
  1. 8
    0
      .eslintrc.js
  2. 14
    14
      app/actions/APIActions.js
  3. 15
    15
      app/actions/auth.js
  4. 6
    6
      app/actions/entities.js
  5. 3
    3
      app/actions/paginated.js
  6. 1
    1
      app/actions/store.js
  7. 233
    42
      app/actions/webplayer.js
  8. 2
    2
      app/common/utils/jquery.js
  9. 6
    6
      app/components/Album.jsx
  10. 2
    2
      app/components/Albums.jsx
  11. 2
    2
      app/components/Artist.jsx
  12. 2
    2
      app/components/Artists.jsx
  13. 1
    1
      app/components/Discover.jsx
  14. 7
    7
      app/components/Login.jsx
  15. 13
    13
      app/components/Songs.jsx
  16. 2
    2
      app/components/elements/DismissibleAlert.jsx
  17. 4
    4
      app/components/elements/FilterBar.jsx
  18. 21
    21
      app/components/elements/Grid.jsx
  19. 2
    2
      app/components/elements/Pagination.jsx
  20. 50
    16
      app/components/elements/WebPlayer.jsx
  21. 5
    5
      app/components/layouts/Sidebar.jsx
  22. 1
    1
      app/components/layouts/Simple.jsx
  23. 1
    1
      app/containers/App.jsx
  24. 9
    9
      app/containers/RequireAuthentication.js
  25. 1
    1
      app/containers/Root.jsx
  26. 1
    1
      app/locales/index.js
  27. 11
    11
      app/locales/messagesDescriptors/Login.js
  28. 4
    4
      app/locales/messagesDescriptors/Songs.js
  29. 4
    4
      app/locales/messagesDescriptors/api.js
  30. 10
    10
      app/locales/messagesDescriptors/common.js
  31. 2
    2
      app/locales/messagesDescriptors/elements/FilterBar.js
  32. 6
    6
      app/locales/messagesDescriptors/elements/Pagination.js
  33. 8
    8
      app/locales/messagesDescriptors/elements/WebPlayer.js
  34. 3
    3
      app/locales/messagesDescriptors/grid.js
  35. 11
    11
      app/locales/messagesDescriptors/layouts/Sidebar.js
  36. 9
    9
      app/middleware/api.js
  37. 3
    3
      app/models/api.js
  38. 2
    2
      app/models/auth.js
  39. 3
    3
      app/models/entities.js
  40. 1
    1
      app/models/i18n.js
  41. 1
    1
      app/models/paginated.js
  42. 1
    1
      app/models/webplayer.js
  43. 9
    9
      app/reducers/auth.js
  44. 12
    6
      app/reducers/entities.js
  45. 2
    2
      app/reducers/index.js
  46. 3
    3
      app/reducers/paginated.js
  47. 84
    13
      app/reducers/webplayer.js
  48. 2
    2
      app/utils/ampache.js
  49. 1
    1
      app/utils/immutable.js
  50. 1
    1
      app/utils/locale.js
  51. 2
    2
      app/utils/misc.js
  52. 3
    3
      app/utils/pagination.js
  53. 2
    2
      app/utils/url.js
  54. 5
    5
      app/views/AlbumPage.jsx
  55. 7
    7
      app/views/AlbumsPage.jsx
  56. 8
    8
      app/views/ArtistPage.jsx
  57. 6
    6
      app/views/ArtistsPage.jsx
  58. 1
    1
      app/views/BrowsePage.jsx
  59. 1
    1
      app/views/DiscoverPage.jsx
  60. 1
    1
      app/views/HomePage.jsx
  61. 8
    8
      app/views/LoginPage.jsx
  62. 3
    3
      app/views/LogoutPage.jsx
  63. 8
    8
      app/views/SongsPage.jsx
  64. 81
    47
      app/views/WebPlayer.jsx
  65. 3
    3
      public/1.1.js
  66. 1
    1
      public/1.1.js.map
  67. 1
    1
      public/fix.ie9.js
  68. 1
    1
      public/fix.ie9.js.map
  69. 27
    26
      public/index.js
  70. 1
    1
      public/index.js.map
  71. 1
    1
      public/style.css

+ 8
- 0
.eslintrc.js View File

@@ -43,6 +43,14 @@ module.exports = {
43 43
         "strict": [
44 44
             "error",
45 45
         ],
46
+        "comma-dangle": [
47
+            "error",
48
+            "always-multiline"
49
+        ],
50
+        "space-before-function-paren": [
51
+            "error",
52
+            { "anonymous": "always", "named": "never" }
53
+        ],
46 54
         "react/jsx-uses-react": "error",
47 55
         "react/jsx-uses-vars": "error",
48 56
 

+ 14
- 14
app/actions/APIActions.js View File

@@ -42,7 +42,7 @@ export default function (action, requestType, successType, failureType) {
42 42
             {
43 43
                 artist: arrayOf(artist),
44 44
                 album: arrayOf(album),
45
-                song: arrayOf(song)
45
+                song: arrayOf(song),
46 46
             },
47 47
             {
48 48
                 // Use custom assignEntity function to delete useless fields
@@ -52,7 +52,7 @@ export default function (action, requestType, successType, failureType) {
52 52
                     } else {
53 53
                         output[key] = value;
54 54
                     }
55
-                }
55
+                },
56 56
             }
57 57
         );
58 58
     };
@@ -80,9 +80,9 @@ export default function (action, requestType, successType, failureType) {
80 80
                     type: itemName,
81 81
                     result: jsonData.result[itemName],
82 82
                     nPages: nPages,
83
-                    currentPage: pageNumber
84
-                }
85
-            }
83
+                    currentPage: pageNumber,
84
+                },
85
+            },
86 86
         ];
87 87
     };
88 88
 
@@ -104,7 +104,7 @@ export default function (action, requestType, successType, failureType) {
104 104
         return {
105 105
             type: requestType,
106 106
             payload: {
107
-            }
107
+            },
108 108
         };
109 109
     };
110 110
 
@@ -119,8 +119,8 @@ export default function (action, requestType, successType, failureType) {
119 119
         return {
120 120
             type: failureType,
121 121
             payload: {
122
-                error: error
123
-            }
122
+                error: error,
123
+            },
124 124
         };
125 125
     };
126 126
 
@@ -144,7 +144,7 @@ export default function (action, requestType, successType, failureType) {
144 144
         // Set extra params for pagination
145 145
         let extraParams = {
146 146
             offset: offset,
147
-            limit: limit
147
+            limit: limit,
148 148
         };
149 149
 
150 150
         // Handle filter
@@ -165,13 +165,13 @@ export default function (action, requestType, successType, failureType) {
165 165
                 dispatch: [
166 166
                     fetchItemsRequest,
167 167
                     null,
168
-                    fetchItemsFailure
168
+                    fetchItemsFailure,
169 169
                 ],
170 170
                 action: action,
171 171
                 auth: passphrase,
172 172
                 username: username,
173
-                extraParams: extraParams
174
-            }
173
+                extraParams: extraParams,
174
+            },
175 175
         };
176 176
     };
177 177
 
@@ -186,7 +186,7 @@ export default function (action, requestType, successType, failureType) {
186 186
      *
187 187
      * Dispatches the CALL_API action to fetch these items.
188 188
      */
189
-    const loadPaginatedItems = function({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
189
+    const loadPaginatedItems = function ({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
190 190
         return (dispatch, getState) => {
191 191
             // Get credentials from the state
192 192
             const { auth } = getState();
@@ -222,7 +222,7 @@ export default function (action, requestType, successType, failureType) {
222 222
      *
223 223
      * Dispatches the CALL_API action to fetch this item.
224 224
      */
225
-    const loadItem = function({ filter = null, include = [] } = {}) {
225
+    const loadItem = function ({ filter = null, include = [] } = {}) {
226 226
         return (dispatch, getState) => {
227 227
             // Get credentials from the state
228 228
             const { auth } = getState();

+ 15
- 15
app/actions/auth.js View File

@@ -41,13 +41,13 @@ export function loginKeepAlive(username, token, endpoint) {
41 41
                 null,
42 42
                 error => dispatch => {
43 43
                     dispatch(loginUserFailure(error || new i18nRecord({ id: "app.login.expired", values: {}})));
44
-                }
44
+                },
45 45
             ],
46 46
             action: "ping",
47 47
             auth: token,
48 48
             username: username,
49
-            extraParams: {}
50
-        }
49
+            extraParams: {},
50
+        },
51 51
     };
52 52
 }
53 53
 
@@ -72,8 +72,8 @@ export function loginUserSuccess(username, token, endpoint, rememberMe, timerID)
72 72
             token: token,
73 73
             endpoint: endpoint,
74 74
             rememberMe: rememberMe,
75
-            timerID: timerID
76
-        }
75
+            timerID: timerID,
76
+        },
77 77
     };
78 78
 }
79 79
 
@@ -94,8 +94,8 @@ export function loginUserFailure(error) {
94 94
     return {
95 95
         type: LOGIN_USER_FAILURE,
96 96
         payload: {
97
-            error: error
98
-        }
97
+            error: error,
98
+        },
99 99
     };
100 100
 }
101 101
 
@@ -111,8 +111,8 @@ export function loginUserExpired(error) {
111 111
     return {
112 112
         type: LOGIN_USER_EXPIRED,
113 113
         payload: {
114
-            error: error
115
-        }
114
+            error: error,
115
+        },
116 116
     };
117 117
 }
118 118
 
@@ -125,7 +125,7 @@ export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST";
125 125
  */
126 126
 export function loginUserRequest() {
127 127
     return {
128
-        type: LOGIN_USER_REQUEST
128
+        type: LOGIN_USER_REQUEST,
129 129
     };
130 130
 }
131 131
 
@@ -152,7 +152,7 @@ export function logout() {
152 152
         Cookies.remove("token");
153 153
         Cookies.remove("endpoint");
154 154
         dispatch({
155
-            type: LOGOUT_USER
155
+            type: LOGOUT_USER,
156 156
         });
157 157
     };
158 158
 }
@@ -223,7 +223,7 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
223 223
                     // Get token from the API
224 224
                     const token = {
225 225
                         token: jsonData.auth,
226
-                        expires: new Date(jsonData.sessionExpire)
226
+                        expires: new Date(jsonData.sessionExpire),
227 227
                     };
228 228
                     // Handle session keep alive timer
229 229
                     const timerID = setInterval(
@@ -242,12 +242,12 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
242 242
                     // Redirect
243 243
                     dispatch(push(redirect));
244 244
                 },
245
-                loginUserFailure
245
+                loginUserFailure,
246 246
             ],
247 247
             action: "handshake",
248 248
             auth: passphrase,
249 249
             username: username,
250
-            extraParams: {timestamp: time}
251
-        }
250
+            extraParams: {timestamp: time},
251
+        },
252 252
     };
253 253
 }

+ 6
- 6
app/actions/entities.js View File

@@ -17,8 +17,8 @@ export function pushEntities(entities, refCountType=["album", "artist", "song"])
17 17
         type: PUSH_ENTITIES,
18 18
         payload: {
19 19
             entities: entities,
20
-            refCountType: refCountType
21
-        }
20
+            refCountType: refCountType,
21
+        },
22 22
     };
23 23
 }
24 24
 
@@ -36,8 +36,8 @@ export function incrementRefCount(entities) {
36 36
     return {
37 37
         type: INCREMENT_REFCOUNT,
38 38
         payload: {
39
-            entities: entities
40
-        }
39
+            entities: entities,
40
+        },
41 41
     };
42 42
 }
43 43
 
@@ -55,7 +55,7 @@ export function decrementRefCount(entities) {
55 55
     return {
56 56
         type: DECREMENT_REFCOUNT,
57 57
         payload: {
58
-            entities: entities
59
-        }
58
+            entities: entities,
59
+        },
60 60
     };
61 61
 }

+ 3
- 3
app/actions/paginated.js View File

@@ -7,8 +7,8 @@ import { decrementRefCount } from "./entities";
7 7
 
8 8
 
9 9
 /** Define an action to invalidate results in paginated store. */
10
-export const CLEAR_RESULTS = "CLEAR_RESULTS";
11
-export function clearResults() {
10
+export const CLEAR_PAGINATED_RESULTS = "CLEAR_PAGINATED_RESULTS";
11
+export function clearPaginatedResults() {
12 12
     return (dispatch, getState) => {
13 13
         // Decrement reference counter
14 14
         const paginatedStore = getState().paginated;
@@ -18,7 +18,7 @@ export function clearResults() {
18 18
 
19 19
         // Clear results in store
20 20
         dispatch({
21
-            type: CLEAR_RESULTS
21
+            type: CLEAR_PAGINATED_RESULTS,
22 22
         });
23 23
     };
24 24
 }

+ 1
- 1
app/actions/store.js View File

@@ -7,6 +7,6 @@
7 7
 export const INVALIDATE_STORE = "INVALIDATE_STORE";
8 8
 export function invalidateStore() {
9 9
     return {
10
-        type: INVALIDATE_STORE
10
+        type: INVALIDATE_STORE,
11 11
     };
12 12
 }

+ 233
- 42
app/actions/webplayer.js View File

@@ -1,93 +1,284 @@
1
-// TODO: This file is not finished
1
+/**
2
+ * These actions are actions acting on the webplayer.
3
+ */
4
+
5
+// Other actions
6
+import { decrementRefCount, incrementRefCount } from "./entities";
7
+
8
+
2 9
 export const PLAY_PAUSE = "PLAY_PAUSE";
3 10
 /**
4
- * true to play, false to pause.
11
+ * Toggle play / pause for the webplayer.
12
+ *
13
+ * @param   playPause   [Optional] True to play, false to pause. If not given,
14
+ *                      toggle the current state.
15
+ *
16
+ * @return  Dispatch a PLAY_PAUSE action.
5 17
  */
6 18
 export function togglePlaying(playPause) {
7 19
     return (dispatch, getState) => {
8
-        let isPlaying = false;
20
+        let newIsPlaying = false;
9 21
         if (typeof playPause !== "undefined") {
10
-            isPlaying = playPause;
22
+            // If we want to force a mode
23
+            newIsPlaying = playPause;
11 24
         } else {
12
-            isPlaying = !(getState().webplayer.isPlaying);
25
+            // Else, just toggle
26
+            newIsPlaying = !(getState().webplayer.isPlaying);
13 27
         }
28
+        // Dispatch action
14 29
         dispatch({
15 30
             type: PLAY_PAUSE,
16 31
             payload: {
17
-                isPlaying: isPlaying
18
-            }
32
+                isPlaying: newIsPlaying,
33
+            },
34
+        });
35
+    };
36
+}
37
+
38
+
39
+export const STOP_PLAYBACK = "STOP_PLAYBACK";
40
+/**
41
+ * Stop the webplayer, clearing the playlist.
42
+ *
43
+ * Handle the entities store reference counting.
44
+ *
45
+ * @return  Dispatch a STOP_PLAYBACK action.
46
+ */
47
+export function stopPlayback() {
48
+    return (dispatch, getState) => {
49
+        // Handle reference counting
50
+        dispatch(decrementRefCount({
51
+            song: getState().webplayer.get("playlist").toArray(),
52
+        }));
53
+        // Stop playback
54
+        dispatch ({
55
+            type: STOP_PLAYBACK,
56
+        });
57
+    };
58
+}
59
+
60
+
61
+export const SET_PLAYLIST = "SET_PLAYLIST";
62
+/**
63
+ * Set a given playlist.
64
+ *
65
+ * Handle the entities store reference counting.
66
+ *
67
+ * @param   playlist    A list of song IDs.
68
+ *
69
+ * @return  Dispatch a SET_PLAYLIST action.
70
+ */
71
+export function setPlaylist(playlist) {
72
+    return (dispatch, getState) => {
73
+        // Handle reference counting
74
+        dispatch(decrementRefCount({
75
+            song: getState().webplayer.get("playlist").toArray(),
76
+        }));
77
+        dispatch(incrementRefCount({
78
+            song: playlist,
79
+        }));
80
+        // Set playlist
81
+        dispatch ({
82
+            type: SET_PLAYLIST,
83
+            payload: {
84
+                playlist: playlist,
85
+            },
19 86
         });
20 87
     };
21 88
 }
22 89
 
23
-export const PUSH_PLAYLIST = "PUSH_PLAYLIST";
24
-export function playTrack(trackID) {
90
+
91
+/**
92
+ * Play a given song, emptying the current playlist.
93
+ *
94
+ * Handle the entities store reference counting.
95
+ *
96
+ * @param   songID      The id of the song to play.
97
+ *
98
+ * @return  Dispatch a SET_PLAYLIST action to play this song and start playing.
99
+ */
100
+export function playSong(songID) {
25 101
     return (dispatch, getState) => {
26
-        const track = getState().api.entities.getIn(["track", trackID]);
27
-        const album = getState().api.entities.getIn(["album", track.get("album")]);
28
-        const artist = getState().api.entities.getIn(["artist", track.get("artist")]);
102
+        // Handle reference counting
103
+        dispatch(decrementRefCount({
104
+            song: getState().webplayer.get("playlist").toArray(),
105
+        }));
106
+        dispatch(incrementRefCount({
107
+            song: [songID],
108
+        }));
109
+        // Set new playlist
29 110
         dispatch({
30
-            type: PUSH_PLAYLIST,
111
+            type: SET_PLAYLIST,
31 112
             payload: {
32
-                playlist: [trackID],
33
-                tracks: [
34
-                    [trackID, track]
35
-                ],
36
-                albums: [
37
-                    [album.get("id"), album]
38
-                ],
39
-                artists: [
40
-                    [artist.get("id"), artist]
41
-                ]
42
-            }
113
+                playlist: [songID],
114
+            },
43 115
         });
116
+        // Force playing
44 117
         dispatch(togglePlaying(true));
45 118
     };
46 119
 }
47 120
 
48
-export const CHANGE_TRACK = "CHANGE_TRACK";
49
-export function playPrevious() {
50
-    // TODO: Playlist overflow
51
-    return (dispatch, getState) => {
52
-        let { index } = getState().webplayer;
121
+
122
+export const PUSH_SONG = "PUSH_SONG";
123
+/**
124
+ * Push a given song in the playlist.
125
+ *
126
+ * Handle the entities store reference counting.
127
+ *
128
+ * @param   songID      The id of the song to push.
129
+ * @param   index       [Optional] The position to insert at in the playlist.
130
+ *                      If negative, counts from the end. Defaults to last.
131
+ *
132
+ * @return  Dispatch a PUSH_SONG action.
133
+ */
134
+export function pushSong(songID, index=-1) {
135
+    return (dispatch) => {
136
+        // Handle reference counting
137
+        dispatch(incrementRefCount({
138
+            song: [songID],
139
+        }));
140
+        // Push song
53 141
         dispatch({
54
-            type: CHANGE_TRACK,
142
+            type: PUSH_SONG,
55 143
             payload: {
56
-                index: index - 1
57
-            }
144
+                song: songID,
145
+                index: index,
146
+            },
58 147
         });
59 148
     };
60 149
 }
61
-export function playNext() {
62
-    // TODO: Playlist overflow
63
-    return (dispatch, getState) => {
64
-        let { index } = getState().webplayer;
150
+
151
+
152
+export const POP_SONG = "POP_SONG";
153
+/**
154
+ * Pop a given song from the playlist.
155
+ *
156
+ * Handle the entities store reference counting.
157
+ *
158
+ * @param   songID      The id of the song to pop.
159
+ *
160
+ * @return  Dispatch a POP_SONG action.
161
+ */
162
+export function popSong(songID) {
163
+    return (dispatch) => {
164
+        // Handle reference counting
165
+        dispatch(decrementRefCount({
166
+            song: [songID],
167
+        }));
168
+        // Pop song
65 169
         dispatch({
66
-            type: CHANGE_TRACK,
170
+            type: POP_SONG,
67 171
             payload: {
68
-                index: index + 1
69
-            }
172
+                song: songID,
173
+            },
70 174
         });
71 175
     };
72 176
 }
73 177
 
178
+
179
+export const JUMP_TO_SONG = "JUMP_TO_SONG";
180
+/**
181
+ * Set current playlist index to specific song.
182
+ *
183
+ * @param   songID      The id of the song to play.
184
+ *
185
+ * @return  Dispatch a JUMP_TO_SONG action.
186
+ */
187
+export function jumpToSong(songID) {
188
+    return (dispatch) => {
189
+        // Push song
190
+        dispatch({
191
+            type: JUMP_TO_SONG,
192
+            payload: {
193
+                song: songID,
194
+            },
195
+        });
196
+    };
197
+}
198
+
199
+
200
+export const PLAY_PREVIOUS = "PLAY_PREVIOUS";
201
+/**
202
+ * Move one song backwards in the playlist.
203
+ *
204
+ * @return  Dispatch a PLAY_PREVIOUS action.
205
+ */
206
+export function playPrevious() {
207
+    return (dispatch) => {
208
+        dispatch({
209
+            type: PLAY_PREVIOUS,
210
+        });
211
+    };
212
+}
213
+
214
+
215
+export const PLAY_NEXT = "PLAY_NEXT";
216
+/**
217
+ * Move one song forward in the playlist.
218
+ *
219
+ * @return  Dispatch a PLAY_NEXT action.
220
+ */
221
+export function playNext() {
222
+    return (dispatch) => {
223
+        dispatch({
224
+            type: PLAY_NEXT,
225
+        });
226
+    };
227
+}
228
+
229
+
74 230
 export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
231
+/**
232
+ * Toggle random mode.
233
+ *
234
+ * @return  Dispatch a TOGGLE_RANDOM action.
235
+ */
75 236
 export function toggleRandom() {
76 237
     return {
77
-        type: TOGGLE_RANDOM
238
+        type: TOGGLE_RANDOM,
78 239
     };
79 240
 }
80 241
 
242
+
81 243
 export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
244
+/**
245
+ * Toggle repeat mode.
246
+ *
247
+ * @return  Dispatch a TOGGLE_REPEAT action.
248
+ */
82 249
 export function toggleRepeat() {
83 250
     return {
84
-        type: TOGGLE_REPEAT
251
+        type: TOGGLE_REPEAT,
85 252
     };
86 253
 }
87 254
 
255
+
88 256
 export const TOGGLE_MUTE = "TOGGLE_MUTE";
257
+/**
258
+ * Toggle mute mode.
259
+ *
260
+ * @return  Dispatch a TOGGLE_MUTE action.
261
+ */
89 262
 export function toggleMute() {
90 263
     return {
91
-        type: TOGGLE_MUTE
264
+        type: TOGGLE_MUTE,
265
+    };
266
+}
267
+
268
+
269
+export const SET_VOLUME = "SET_VOLUME";
270
+/**
271
+ * Set the volume.
272
+ *
273
+ * @param   volume      Volume to set (between 0 and 100)
274
+ *
275
+ * @return  Dispatch a SET_VOLUME action.
276
+ */
277
+export function setVolume(volume) {
278
+    return {
279
+        type: SET_VOLUME,
280
+        payload: {
281
+            volume: volume,
282
+        },
92 283
     };
93 284
 }

+ 2
- 2
app/common/utils/jquery.js View File

@@ -12,8 +12,8 @@
12 12
  *
13 13
  * @return  The element it was applied one, for chaining.
14 14
  */
15
-$.fn.shake = function(intShakes, intDistance, intDuration) {
16
-    this.each(function() {
15
+$.fn.shake = function (intShakes, intDistance, intDuration) {
16
+    this.each(function () {
17 17
         $(this).css("position","relative");
18 18
         for (let x=1; x<=intShakes; x++) {
19 19
             $(this).animate({left:(intDistance*-1)}, (((intDuration/intShakes)/4)))

+ 6
- 6
app/components/Album.jsx View File

@@ -22,7 +22,7 @@ const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages
22 22
  * Track row in an album tracks table.
23 23
  */
24 24
 class AlbumTrackRowCSSIntl extends Component {
25
-    render () {
25
+    render() {
26 26
         const { formatMessage } = this.props.intl;
27 27
         const length = formatLength(this.props.track.get("time"));
28 28
         return (
@@ -45,7 +45,7 @@ class AlbumTrackRowCSSIntl extends Component {
45 45
 AlbumTrackRowCSSIntl.propTypes = {
46 46
     playAction: PropTypes.func.isRequired,
47 47
     track: PropTypes.instanceOf(Immutable.Map).isRequired,
48
-    intl: intlShape.isRequired
48
+    intl: intlShape.isRequired,
49 49
 };
50 50
 export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
51 51
 
@@ -54,7 +54,7 @@ export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
54 54
  * Tracks table of an album.
55 55
  */
56 56
 class AlbumTracksTableCSS extends Component {
57
-    render () {
57
+    render() {
58 58
         let rows = [];
59 59
         // Build rows for each track
60 60
         const playAction = this.props.playAction;
@@ -72,7 +72,7 @@ class AlbumTracksTableCSS extends Component {
72 72
 }
73 73
 AlbumTracksTableCSS.propTypes = {
74 74
     playAction: PropTypes.func.isRequired,
75
-    tracks: PropTypes.instanceOf(Immutable.List).isRequired
75
+    tracks: PropTypes.instanceOf(Immutable.List).isRequired,
76 76
 };
77 77
 export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
78 78
 
@@ -81,7 +81,7 @@ export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
81 81
  * An entire album row containing art and tracks table.
82 82
  */
83 83
 class AlbumRowCSS extends Component {
84
-    render () {
84
+    render() {
85 85
         return (
86 86
             <div className="row" styleName="row">
87 87
                 <div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow">
@@ -104,6 +104,6 @@ class AlbumRowCSS extends Component {
104 104
 AlbumRowCSS.propTypes = {
105 105
     playAction: PropTypes.func.isRequired,
106 106
     album: PropTypes.instanceOf(Immutable.Map).isRequired,
107
-    songs: PropTypes.instanceOf(Immutable.List).isRequired
107
+    songs: PropTypes.instanceOf(Immutable.List).isRequired,
108 108
 };
109 109
 export let AlbumRow = CSSModules(AlbumRowCSS, css);

+ 2
- 2
app/components/Albums.jsx View File

@@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
11 11
  * Paginated albums grid
12 12
  */
13 13
 export default class Albums extends Component {
14
-    render () {
14
+    render() {
15 15
         // Handle error
16 16
         let error = null;
17 17
         if (this.props.error) {
@@ -25,7 +25,7 @@ export default class Albums extends Component {
25 25
             itemsType: "album",
26 26
             itemsLabel: "app.common.album",
27 27
             subItemsType: "tracks",
28
-            subItemsLabel: "app.common.track"
28
+            subItemsLabel: "app.common.track",
29 29
         };
30 30
 
31 31
         return (

+ 2
- 2
app/components/Artist.jsx View File

@@ -26,7 +26,7 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], commonMessage
26 26
  * Single artist page
27 27
  */
28 28
 class ArtistCSS extends Component {
29
-    render () {
29
+    render() {
30 30
         // Define loading message
31 31
         let loading = null;
32 32
         if (this.props.isFetching) {
@@ -87,6 +87,6 @@ ArtistCSS.propTypes = {
87 87
     playAction: PropTypes.func.isRequired,
88 88
     artist: PropTypes.instanceOf(Immutable.Map),
89 89
     albums: PropTypes.instanceOf(Immutable.List),
90
-    songs: PropTypes.instanceOf(Immutable.Map)
90
+    songs: PropTypes.instanceOf(Immutable.Map),
91 91
 };
92 92
 export default CSSModules(ArtistCSS, css);

+ 2
- 2
app/components/Artists.jsx View File

@@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
11 11
  * Paginated artists grid
12 12
  */
13 13
 export default class Artists extends Component {
14
-    render () {
14
+    render() {
15 15
         // Handle error
16 16
         let error = null;
17 17
         if (this.props.error) {
@@ -25,7 +25,7 @@ export default class Artists extends Component {
25 25
             itemsType: "artist",
26 26
             itemsLabel: "app.common.artist",
27 27
             subItemsType: "albums",
28
-            subItemsLabel: "app.common.album"
28
+            subItemsLabel: "app.common.album",
29 29
         };
30 30
 
31 31
         return (

+ 1
- 1
app/components/Discover.jsx View File

@@ -6,7 +6,7 @@ import FontAwesome from "react-fontawesome";
6 6
 import css from "../styles/Discover.scss";
7 7
 
8 8
 export default class DiscoverCSS extends Component {
9
-    render () {
9
+    render() {
10 10
         const artistsAlbumsSongsDropdown = (
11 11
             <div className="btn-group">
12 12
                 <button type="button" className="btn btn-default dropdown-toggle" styleName="h2Title" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

+ 7
- 7
app/components/Login.jsx View File

@@ -23,7 +23,7 @@ const loginMessages = defineMessages(messagesMap(Array.concat([], APIMessages, m
23 23
  * Login form component
24 24
  */
25 25
 class LoginFormCSSIntl extends Component {
26
-    constructor (props) {
26
+    constructor(props) {
27 27
         super(props);
28 28
         this.handleSubmit = this.handleSubmit.bind(this);  // bind this to handleSubmit
29 29
     }
@@ -36,7 +36,7 @@ class LoginFormCSSIntl extends Component {
36 36
      *
37 37
      * @return  True if an error is set, false otherwise
38 38
      */
39
-    setError (formGroup, hasError) {
39
+    setError(formGroup, hasError) {
40 40
         if (hasError) {
41 41
             // If error is true, then add error class
42 42
             formGroup.classList.add("has-error");
@@ -54,7 +54,7 @@ class LoginFormCSSIntl extends Component {
54 54
      *
55 55
      * @param   e   JS Event.
56 56
      */
57
-    handleSubmit (e) {
57
+    handleSubmit(e) {
58 58
         e.preventDefault();
59 59
 
60 60
         // Don't handle submit if already logging in
@@ -79,7 +79,7 @@ class LoginFormCSSIntl extends Component {
79 79
         }
80 80
     }
81 81
 
82
-    componentDidUpdate () {
82
+    componentDidUpdate() {
83 83
         if (this.props.error) {
84 84
             // On unsuccessful login, set error classes and shake the form
85 85
             $(this.refs.loginForm).shake(3, 10, 300);
@@ -89,7 +89,7 @@ class LoginFormCSSIntl extends Component {
89 89
         }
90 90
     }
91 91
 
92
-    render () {
92
+    render() {
93 93
         const {formatMessage} = this.props.intl;
94 94
 
95 95
         // Handle info message
@@ -187,7 +187,7 @@ export let LoginForm = injectIntl(CSSModules(LoginFormCSSIntl, css));
187 187
  * Main login page, including title and login form.
188 188
  */
189 189
 class LoginCSS extends Component {
190
-    render () {
190
+    render() {
191 191
         const greeting = (
192 192
             <p>
193 193
                 <FormattedMessage {...loginMessages["app.login.greeting"]} />
@@ -212,6 +212,6 @@ LoginCSS.propTypes = {
212 212
     onSubmit: PropTypes.func.isRequired,
213 213
     isAuthenticating: PropTypes.bool,
214 214
     info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
215
-    error: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
215
+    error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
216 216
 };
217 217
 export default CSSModules(LoginCSS, css);

+ 13
- 13
app/components/Songs.jsx View File

@@ -30,7 +30,7 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
30 30
  * A single row for a single song in the songs table.
31 31
  */
32 32
 class SongsTableRowCSSIntl extends Component {
33
-    render () {
33
+    render() {
34 34
         const { formatMessage } = this.props.intl;
35 35
 
36 36
         const length = formatLength(this.props.song.get("time"));
@@ -59,7 +59,7 @@ class SongsTableRowCSSIntl extends Component {
59 59
 SongsTableRowCSSIntl.propTypes = {
60 60
     playAction: PropTypes.func.isRequired,
61 61
     song: PropTypes.instanceOf(Immutable.Map).isRequired,
62
-    intl: intlShape.isRequired
62
+    intl: intlShape.isRequired,
63 63
 };
64 64
 export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
65 65
 
@@ -68,7 +68,7 @@ export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
68 68
  * The songs table.
69 69
  */
70 70
 class SongsTableCSS extends Component {
71
-    render () {
71
+    render() {
72 72
         // Handle filtering
73 73
         let displayedSongs = this.props.songs;
74 74
         if (this.props.filterText) {
@@ -78,7 +78,7 @@ class SongsTableCSS extends Component {
78 78
                 {
79 79
                     "keys": ["name"],
80 80
                     "threshold": 0.4,
81
-                    "include": ["score"]
81
+                    "include": ["score"],
82 82
                 }).search(this.props.filterText);
83 83
             // Keep only items in results
84 84
             displayedSongs = displayedSongs.map(function (item) { return new Immutable.Map(item.item); });
@@ -135,7 +135,7 @@ class SongsTableCSS extends Component {
135 135
 SongsTableCSS.propTypes = {
136 136
     playAction: PropTypes.func.isRequired,
137 137
     songs: PropTypes.instanceOf(Immutable.List).isRequired,
138
-    filterText: PropTypes.string
138
+    filterText: PropTypes.string,
139 139
 };
140 140
 export let SongsTable = CSSModules(SongsTableCSS, css);
141 141
 
@@ -144,10 +144,10 @@ export let SongsTable = CSSModules(SongsTableCSS, css);
144 144
  * Complete songs table view with filter and pagination
145 145
  */
146 146
 export default class FilterablePaginatedSongsTable extends Component {
147
-    constructor (props) {
147
+    constructor(props) {
148 148
         super(props);
149 149
         this.state = {
150
-            filterText: ""  // Initial state, no filter text
150
+            filterText: "",  // Initial state, no filter text
151 151
         };
152 152
 
153 153
         this.handleUserInput = this.handleUserInput.bind(this);  // Bind this on user input handling
@@ -160,13 +160,13 @@ export default class FilterablePaginatedSongsTable extends Component {
160 160
      *
161 161
      * @param   filterText  Content of the filter input.
162 162
      */
163
-    handleUserInput (filterText) {
163
+    handleUserInput(filterText) {
164 164
         this.setState({
165
-            filterText: filterText
165
+            filterText: filterText,
166 166
         });
167 167
     }
168 168
 
169
-    render () {
169
+    render() {
170 170
         // Handle error
171 171
         let error = null;
172 172
         if (this.props.error) {
@@ -176,13 +176,13 @@ export default class FilterablePaginatedSongsTable extends Component {
176 176
         // Set props
177 177
         const filterProps = {
178 178
             filterText: this.state.filterText,
179
-            onUserInput: this.handleUserInput
179
+            onUserInput: this.handleUserInput,
180 180
         };
181 181
         const songsTableProps = {
182 182
             playAction: this.props.playAction,
183 183
             isFetching: this.props.isFetching,
184 184
             songs: this.props.songs,
185
-            filterText: this.state.filterText
185
+            filterText: this.state.filterText,
186 186
         };
187 187
 
188 188
         return (
@@ -200,5 +200,5 @@ FilterablePaginatedSongsTable.propTypes = {
200 200
     isFetching: PropTypes.bool.isRequired,
201 201
     error: PropTypes.string,
202 202
     songs: PropTypes.instanceOf(Immutable.List).isRequired,
203
-    pagination: PropTypes.object.isRequired
203
+    pagination: PropTypes.object.isRequired,
204 204
 };

+ 2
- 2
app/components/elements/DismissibleAlert.jsx View File

@@ -6,7 +6,7 @@ import React, { Component, PropTypes } from "react";
6 6
  * A dismissible Bootstrap alert.
7 7
  */
8 8
 export default class DismissibleAlert extends Component {
9
-    render () {
9
+    render() {
10 10
         // Set correct alert type
11 11
         let alertType = "alert-danger";
12 12
         if (this.props.type) {
@@ -27,5 +27,5 @@ export default class DismissibleAlert extends Component {
27 27
 }
28 28
 DismissibleAlert.propTypes = {
29 29
     type: PropTypes.string,
30
-    text: PropTypes.string
30
+    text: PropTypes.string,
31 31
 };

+ 4
- 4
app/components/elements/FilterBar.jsx View File

@@ -20,7 +20,7 @@ const filterMessages = defineMessages(messagesMap(Array.concat([], messages)));
20 20
  * Filter bar element with input filter.
21 21
  */
22 22
 class FilterBarCSSIntl extends Component {
23
-    constructor (props) {
23
+    constructor(props) {
24 24
         super(props);
25 25
         // Bind this on methods
26 26
         this.handleChange = this.handleChange.bind(this);
@@ -33,12 +33,12 @@ class FilterBarCSSIntl extends Component {
33 33
      *
34 34
      * @param   e   A JS event.
35 35
      */
36
-    handleChange (e) {
36
+    handleChange(e) {
37 37
         e.preventDefault();
38 38
         this.props.onUserInput(this.refs.filterTextInput.value);
39 39
     }
40 40
 
41
-    render () {
41
+    render() {
42 42
         const {formatMessage} = this.props.intl;
43 43
 
44 44
         return (
@@ -60,6 +60,6 @@ class FilterBarCSSIntl extends Component {
60 60
 FilterBarCSSIntl.propTypes = {
61 61
     onUserInput: PropTypes.func,
62 62
     filterText: PropTypes.string,
63
-    intl: intlShape.isRequired
63
+    intl: intlShape.isRequired,
64 64
 };
65 65
 export default injectIntl(CSSModules(FilterBarCSSIntl, css));

+ 21
- 21
app/components/elements/Grid.jsx View File

@@ -31,7 +31,7 @@ const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages,
31 31
 const ISOTOPE_OPTIONS = {  /** Default options for Isotope grid layout. */
32 32
     getSortData: {
33 33
         name: ".name",
34
-        nSubitems: ".sub-items .n-sub-items"
34
+        nSubitems: ".sub-items .n-sub-items",
35 35
     },
36 36
     transitionDuration: 0,
37 37
     sortBy: "name",
@@ -40,8 +40,8 @@ const ISOTOPE_OPTIONS = {  /** Default options for Isotope grid layout. */
40 40
     layoutMode: "fitRows",
41 41
     filter: "*",
42 42
     fitRows: {
43
-        gutter: 0
44
-    }
43
+        gutter: 0,
44
+    },
45 45
 };
46 46
 
47 47
 
@@ -49,7 +49,7 @@ const ISOTOPE_OPTIONS = {  /** Default options for Isotope grid layout. */
49 49
  * A single item in the grid, art + text under the art.
50 50
  */
51 51
 class GridItemCSSIntl extends Component {
52
-    render () {
52
+    render() {
53 53
         const {formatMessage} = this.props.intl;
54 54
 
55 55
         // Get number of sub-items
@@ -85,7 +85,7 @@ GridItemCSSIntl.propTypes = {
85 85
     itemsLabel: PropTypes.string.isRequired,
86 86
     subItemsType: PropTypes.string.isRequired,
87 87
     subItemsLabel: PropTypes.string.isRequired,
88
-    intl: intlShape.isRequired
88
+    intl: intlShape.isRequired,
89 89
 };
90 90
 export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
91 91
 
@@ -94,7 +94,7 @@ export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
94 94
  * A grid, formatted using Isotope.JS
95 95
  */
96 96
 export class Grid extends Component {
97
-    constructor (props) {
97
+    constructor(props) {
98 98
         super(props);
99 99
 
100 100
         // Init grid data member
@@ -108,7 +108,7 @@ export class Grid extends Component {
108 108
     /**
109 109
      * Create an isotope container if none already exist.
110 110
      */
111
-    createIsotopeContainer () {
111
+    createIsotopeContainer() {
112 112
         if (this.iso == null) {
113 113
             this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
114 114
         }
@@ -117,7 +117,7 @@ export class Grid extends Component {
117 117
     /**
118 118
      * Handle filtering on the grid.
119 119
      */
120
-    handleFiltering (props) {
120
+    handleFiltering(props) {
121 121
         // If no query provided, drop any filter in use
122 122
         if (props.filterText == "") {
123 123
             return this.iso.arrange(ISOTOPE_OPTIONS);
@@ -129,7 +129,7 @@ export class Grid extends Component {
129 129
             {
130 130
                 "keys": ["name"],
131 131
                 "threshold": 0.4,
132
-                "include": ["score"]
132
+                "include": ["score"],
133 133
             }
134 134
         ).search(props.filterText);
135 135
 
@@ -149,9 +149,9 @@ export class Grid extends Component {
149 149
                         }
150 150
                         return p;
151 151
                     }, 0);
152
-                }
152
+                },
153 153
             },
154
-            sortBy: "relevance"
154
+            sortBy: "relevance",
155 155
         });
156 156
         this.iso.updateSortData();
157 157
         this.iso.arrange();
@@ -169,7 +169,7 @@ export class Grid extends Component {
169 169
         }
170 170
     }
171 171
 
172
-    componentDidMount () {
172
+    componentDidMount() {
173 173
         // Setup grid
174 174
         this.createIsotopeContainer();
175 175
         // Only arrange if there are elements to arrange
@@ -212,7 +212,7 @@ export class Grid extends Component {
212 212
         }
213 213
 
214 214
         // Layout again after images are loaded
215
-        imagesLoaded(this.refs.grid).on("progress", function() {
215
+        imagesLoaded(this.refs.grid).on("progress", function () {
216 216
             // Layout after each image load, fix for responsive grid
217 217
             if (!iso) {  // Grid could have been destroyed in the meantime
218 218
                 return;
@@ -221,7 +221,7 @@ export class Grid extends Component {
221 221
         });
222 222
     }
223 223
 
224
-    render () {
224
+    render() {
225 225
         // Handle loading
226 226
         let loading = null;
227 227
         if (this.props.isFetching) {
@@ -264,7 +264,7 @@ Grid.propTypes = {
264 264
     itemsLabel: PropTypes.string.isRequired,
265 265
     subItemsType: PropTypes.string.isRequired,
266 266
     subItemsLabel: PropTypes.string.isRequired,
267
-    filterText: PropTypes.string
267
+    filterText: PropTypes.string,
268 268
 };
269 269
 
270 270
 
@@ -272,11 +272,11 @@ Grid.propTypes = {
272 272
  * Full grid with pagination and filtering input.
273 273
  */
274 274
 export default class FilterablePaginatedGrid extends Component {
275
-    constructor (props) {
275
+    constructor(props) {
276 276
         super(props);
277 277
 
278 278
         this.state = {
279
-            filterText: ""  // No filterText at init
279
+            filterText: "",  // No filterText at init
280 280
         };
281 281
 
282 282
         // Bind this
@@ -290,13 +290,13 @@ export default class FilterablePaginatedGrid extends Component {
290 290
      *
291 291
      * @param   filterText  Content of the filter input.
292 292
      */
293
-    handleUserInput (filterText) {
293
+    handleUserInput(filterText) {
294 294
         this.setState({
295
-            filterText: filterText
295
+            filterText: filterText,
296 296
         });
297 297
     }
298 298
 
299
-    render () {
299
+    render() {
300 300
         return (
301 301
             <div>
302 302
                 <FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
@@ -309,5 +309,5 @@ export default class FilterablePaginatedGrid extends Component {
309 309
 
310 310
 FilterablePaginatedGrid.propTypes = {
311 311
     grid: PropTypes.object.isRequired,
312
-    pagination: PropTypes.object.isRequired
312
+    pagination: PropTypes.object.isRequired,
313 313
 };

+ 2
- 2
app/components/elements/Pagination.jsx View File

@@ -22,7 +22,7 @@ const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMes
22 22
  * Pagination button bar
23 23
  */
24 24
 class PaginationCSSIntl extends Component {
25
-    constructor (props) {
25
+    constructor(props) {
26 26
         super (props);
27 27
 
28 28
         // Bind this
@@ -74,7 +74,7 @@ class PaginationCSSIntl extends Component {
74 74
         $(this.refs.paginationModal).modal("hide");
75 75
     }
76 76
 
77
-    render () {
77
+    render() {
78 78
         const { formatMessage } = this.props.intl;
79 79
 
80 80
         // Get bounds

+ 50
- 16
app/components/elements/WebPlayer.jsx View File

@@ -1,47 +1,66 @@
1
-// TODO: This file is to review
1
+// NPM imports
2 2
 import React, { Component, PropTypes } from "react";
3 3
 import CSSModules from "react-css-modules";
4 4
 import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
5
-import FontAwesome from "react-fontawesome";
6 5
 import Immutable from "immutable";
6
+import FontAwesome from "react-fontawesome";
7 7
 
8
+// Local imports
8 9
 import { messagesMap } from "../../utils";
9 10
 
11
+// Styles
10 12
 import css from "../../styles/elements/WebPlayer.scss";
11 13
 
14
+// Translations
12 15
 import commonMessages from "../../locales/messagesDescriptors/common";
13 16
 import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
14 17
 
18
+// Define translations
15 19
 const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
16 20
 
21
+
22
+/**
23
+ * Webplayer component.
24
+ */
17 25
 class WebPlayerCSSIntl extends Component {
18
-    constructor (props) {
26
+    constructor(props) {
19 27
         super(props);
20 28
 
29
+        // Bind this
21 30
         this.artOpacityHandler = this.artOpacityHandler.bind(this);
22 31
     }
23 32
 
24
-    artOpacityHandler (ev) {
33
+    /**
34
+     * Handle opacity on album art.
35
+     *
36
+     * Set opacity on mouseover / mouseout.
37
+     *
38
+     * @param   ev      A JS event.
39
+     */
40
+    artOpacityHandler(ev) {
25 41
         if (ev.type == "mouseover") {
42
+            // On mouse over, reduce opacity
26 43
             this.refs.art.style.opacity = "1";
27 44
             this.refs.artText.style.display = "none";
28 45
         } else {
46
+            // On mouse out, set opacity back
29 47
             this.refs.art.style.opacity = "0.75";
30 48
             this.refs.artText.style.display = "block";
31 49
         }
32 50
     }
33 51
 
34
-    render () {
52
+    render() {
35 53
         const { formatMessage } = this.props.intl;
36 54
 
37
-        const song = this.props.currentTrack;
38
-        if (!song) {
39
-            return (<div></div>);
40
-        }
55
+        // Get current song (eventually undefined)
56
+        const song = this.props.currentSong;
41 57
 
58
+        // Current status (play or pause) for localization
42 59
         const playPause = this.props.isPlaying ? "pause" : "play";
43
-        const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
60
+        // Volume fontawesome icon
61
+        const volumeIcon = this.props.isMute ? "volume-off" : "volume-up";
44 62
 
63
+        // Get classes for random and repeat buttons
45 64
         const randomBtnStyles = ["randomBtn"];
46 65
         const repeatBtnStyles = ["repeatBtn"];
47 66
         if (this.props.isRandom) {
@@ -51,18 +70,30 @@ class WebPlayerCSSIntl extends Component {
51 70
             repeatBtnStyles.push("active");
52 71
         }
53 72
 
73
+        // Check if a song is currently playing
74
+        let art = null;
75
+        let songTitle = null;
76
+        let artistName = null;
77
+        if (song) {
78
+            art = song.get("art");
79
+            songTitle = song.get("title");
80
+            if (this.props.currentArtist) {
81
+                artistName = this.props.currentArtist.get("name");
82
+            }
83
+        }
84
+
54 85
         return (
55 86
             <div id="row" styleName="webplayer">
56 87
                 <div className="col-xs-12">
57 88
                     <div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
58 89
                         <div className="col-xs-12">
59
-                            <img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
90
+                            <img src={art} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
60 91
                             <div ref="artText">
61
-                                <h2>{song.get("title")}</h2>
92
+                                <h2>{songTitle}</h2>
62 93
                                 <h3>
63 94
                                     <span className="text-capitalize">
64 95
                                         <FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
65
-                                    </span> { this.props.currentArtist.get("name") }
96
+                                    </span> { artistName }
66 97
                                 </h3>
67 98
                             </div>
68 99
                         </div>
@@ -82,7 +113,7 @@ class WebPlayerCSSIntl extends Component {
82 113
                         </div>
83 114
                         <div className="col-xs-12">
84 115
                             <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
85
-                                <FontAwesome name={volumeMute} />
116
+                                <FontAwesome name={volumeIcon} />
86 117
                             </button>
87 118
                             <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}>
88 119
                                 <FontAwesome name="repeat" />
@@ -106,7 +137,10 @@ WebPlayerCSSIntl.propTypes = {
106 137
     isRandom: PropTypes.bool.isRequired,
107 138
     isRepeat: PropTypes.bool.isRequired,
108 139
     isMute: PropTypes.bool.isRequired,
109
-    currentTrack: PropTypes.instanceOf(Immutable.Map),
140
+    volume: PropTypes.number.isRequired,
141
+    currentIndex: PropTypes.number.isRequired,
142
+    playlist: PropTypes.instanceOf(Immutable.List).isRequired,
143
+    currentSong: PropTypes.instanceOf(Immutable.Map),
110 144
     currentArtist: PropTypes.instanceOf(Immutable.Map),
111 145
     onPlayPause: PropTypes.func.isRequired,
112 146
     onPrev: PropTypes.func.isRequired,
@@ -114,7 +148,7 @@ WebPlayerCSSIntl.propTypes = {
114 148
     onRandom: PropTypes.func.isRequired,
115 149
     onRepeat: PropTypes.func.isRequired,
116 150
     onMute: PropTypes.func.isRequired,
117
-    intl: intlShape.isRequired
151
+    intl: intlShape.isRequired,
118 152
 };
119 153
 
120 154
 export default injectIntl(CSSModules(WebPlayerCSSIntl, css, { allowMultiple: true }));

+ 5
- 5
app/components/layouts/Sidebar.jsx View File

@@ -8,7 +8,7 @@ import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-i
8 8
 import { messagesMap } from "../../utils";
9 9
 
10 10
 // Other components
11
-/* import WebPlayer from "../../views/WebPlayer"; TODO */
11
+import WebPlayer from "../../views/WebPlayer";
12 12
 
13 13
 // Translations
14 14
 import commonMessages from "../../locales/messagesDescriptors/common";
@@ -25,7 +25,7 @@ const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], common
25 25
  * Sidebar layout component, putting children next to the sidebar menu.
26 26
  */
27 27
 class SidebarLayoutIntl extends Component {
28
-    render () {
28
+    render() {
29 29
         const { formatMessage } = this.props.intl;
30 30
 
31 31
         // Check active links
@@ -35,7 +35,7 @@ class SidebarLayoutIntl extends Component {
35 35
             artists: (this.props.location.pathname == "/artists") ? "active" : "link",
36 36
             albums: (this.props.location.pathname == "/albums") ? "active" : "link",
37 37
             songs: (this.props.location.pathname == "/songs") ? "active" : "link",
38
-            search: (this.props.location.pathname == "/search") ? "active" : "link"
38
+            search: (this.props.location.pathname == "/search") ? "active" : "link",
39 39
         };
40 40
 
41 41
         // Hamburger collapsing function
@@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
146 146
                                 </li>
147 147
                             </ul>
148 148
                         </nav>
149
-                        { /** TODO <WebPlayer /> */ }
149
+                        <WebPlayer />
150 150
                     </div>
151 151
                 </div>
152 152
 
@@ -159,6 +159,6 @@ class SidebarLayoutIntl extends Component {
159 159
 }
160 160
 SidebarLayoutIntl.propTypes = {
161 161
     children: PropTypes.node,
162
-    intl: intlShape.isRequired
162
+    intl: intlShape.isRequired,
163 163
 };
164 164
 export default injectIntl(CSSModules(SidebarLayoutIntl, css));

+ 1
- 1
app/components/layouts/Simple.jsx View File

@@ -6,7 +6,7 @@ import React, { Component } from "react";
6 6
  * Simple layout, meaning just enclosing children in a div.
7 7
  */
8 8
 export default class SimpleLayout extends Component {
9
-    render () {
9
+    render() {
10 10
         return (
11 11
             <div>
12 12
                 {this.props.children}

+ 1
- 1
app/containers/App.jsx View File

@@ -6,7 +6,7 @@
6 6
 import React, { Component, PropTypes } from "react";
7 7
 
8 8
 export default class App extends Component {
9
-    render () {
9
+    render() {
10 10
         return (
11 11
             <div>
12 12
                 {this.props.children}

+ 9
- 9
app/containers/RequireAuthentication.js View File

@@ -7,12 +7,12 @@ import { connect } from "react-redux";
7 7
 
8 8
 
9 9
 export class RequireAuthentication extends Component {
10
-    componentWillMount () {
10
+    componentWillMount() {
11 11
         // Check authentication on mount
12 12
         this.checkAuth(this.props.isAuthenticated);
13 13
     }
14 14
 
15
-    componentWillUpdate (newProps) {
15
+    componentWillUpdate(newProps) {
16 16
         // Check authentication on update
17 17
         this.checkAuth(newProps.isAuthenticated);
18 18
     }
@@ -23,20 +23,20 @@ export class RequireAuthentication extends Component {
23 23
      * @param   isAuthenticated     A boolean stating whether user has a valid
24 24
      *                              session or not.
25 25
      */
26
-    checkAuth (isAuthenticated) {
26
+    checkAuth(isAuthenticated) {
27 27
         if (!isAuthenticated) {
28 28
             // Redirect to login, redirecting to the actual page after login.
29 29
             this.context.router.replace({
30 30
                 pathname: "/login",
31 31
                 state: {
32 32
                     nextPathname: this.props.location.pathname,
33
-                    nextQuery: this.props.location.query
34
-                }
33
+                    nextQuery: this.props.location.query,
34
+                },
35 35
             });
36 36
         }
37 37
     }
38 38
 
39
-    render () {
39
+    render() {
40 40
         return (
41 41
             <div>
42 42
                 {this.props.isAuthenticated === true
@@ -50,15 +50,15 @@ export class RequireAuthentication extends Component {
50 50
 
51 51
 RequireAuthentication.propTypes = {
52 52
     // Injected by React Router
53
-    children: PropTypes.node
53
+    children: PropTypes.node,
54 54
 };
55 55
 
56 56
 RequireAuthentication.contextTypes = {
57
-    router: PropTypes.object.isRequired
57
+    router: PropTypes.object.isRequired,
58 58
 };
59 59
 
60 60
 const mapStateToProps = (state) => ({
61
-    isAuthenticated: state.auth.isAuthenticated
61
+    isAuthenticated: state.auth.isAuthenticated,
62 62
 });
63 63
 
64 64
 export default connect(mapStateToProps)(RequireAuthentication);

+ 1
- 1
app/containers/Root.jsx View File

@@ -27,5 +27,5 @@ Root.propTypes = {
27 27
     render: PropTypes.func,
28 28
     locale: PropTypes.string.isRequired,
29 29
     messages: PropTypes.object.isRequired,
30
-    defaultLocale: PropTypes.string.isRequired
30
+    defaultLocale: PropTypes.string.isRequired,
31 31
 };

+ 1
- 1
app/locales/index.js View File

@@ -1,5 +1,5 @@
1 1
 // Export all the existing locales
2 2
 module.exports = {
3 3
     "en-US": require("./en-US"),
4
-    "fr-FR": require("./fr-FR")
4
+    "fr-FR": require("./fr-FR"),
5 5
 };

+ 11
- 11
app/locales/messagesDescriptors/Login.js View File

@@ -2,55 +2,55 @@ const messages = [
2 2
     {
3 3
         id: "app.login.username",
4 4
         defaultMessage: "Username",
5
-        description: "Username input placeholder"
5
+        description: "Username input placeholder",
6 6
     },
7 7
     {
8 8
         id: "app.login.password",
9 9
         defaultMessage: "Password",
10
-        description: "Password input placeholder"
10
+        description: "Password input placeholder",
11 11
     },
12 12
     {
13 13
         id: "app.login.signIn",
14 14
         defaultMessage: "Sign in",
15
-        description: "Sign in"
15
+        description: "Sign in",
16 16
     },
17 17
     {
18 18
         id: "app.login.endpointInputAriaLabel",
19 19
         defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)",
20
-        description: "ARIA label for the endpoint input"
20
+        description: "ARIA label for the endpoint input",
21 21
     },
22 22
     {
23 23
         id: "app.login.rememberMe",
24 24
         description: "Remember me checkbox label",
25
-        defaultMessage: "Remember me"
25
+        defaultMessage: "Remember me",
26 26
     },
27 27
     {
28 28
         id: "app.login.greeting",
29 29
         description: "Greeting to welcome the user to the app",
30
-        defaultMessage: "Welcome back on Ampache, let's go!"
30
+        defaultMessage: "Welcome back on Ampache, let's go!",
31 31
     },
32 32
 
33 33
     // From the auth reducer
34 34
     {
35 35
         id: "app.login.connecting",
36 36
         defaultMessage: "Connecting…",
37
-        description: "Info message while trying to connect"
37
+        description: "Info message while trying to connect",
38 38
     },
39 39
     {
40 40
         id: "app.login.success",
41 41
         defaultMessage: "Successfully logged in as { username }!",
42
-        description: "Info message on successful login."
42
+        description: "Info message on successful login.",
43 43
     },
44 44
     {
45 45
         id: "app.login.byebye",
46 46
         defaultMessage: "See you soon!",
47
-        description: "Info message on successful logout"
47
+        description: "Info message on successful logout",
48 48
     },
49 49
     {
50 50
         id: "app.login.expired",
51 51
         defaultMessage: "Your session expired… =(",
52
-        description: "Error message on expired session"
53
-    }
52
+        description: "Error message on expired session",
53
+    },
54 54
 ];
55 55
 
56 56
 export default messages;

+ 4
- 4
app/locales/messagesDescriptors/Songs.js View File

@@ -2,18 +2,18 @@ const messages = [
2 2
     {
3 3
         "id": "app.songs.title",
4 4
         "description": "Title (song)",
5
-        "defaultMessage": "Title"
5
+        "defaultMessage": "Title",
6 6
     },
7 7
     {
8 8
         "id": "app.songs.genre",
9 9
         "description": "Genre (song)",
10
-        "defaultMessage": "Genre"
10
+        "defaultMessage": "Genre",
11 11
     },
12 12
     {
13 13
         "id": "app.songs.length",
14 14
         "description": "Length (song)",
15
-        "defaultMessage": "Length"
16
-    }
15
+        "defaultMessage": "Length",
16
+    },
17 17
 ];
18 18
 
19 19
 export default messages;

+ 4
- 4
app/locales/messagesDescriptors/api.js View File

@@ -2,18 +2,18 @@ const messages = [
2 2
     {
3 3
         id: "app.api.invalidResponse",
4 4
         defaultMessage: "Invalid response text.",
5
-        description: "Invalid response from the API"
5
+        description: "Invalid response from the API",
6 6
     },
7 7
     {
8 8
         id: "app.api.emptyResponse",
9 9
         defaultMessage: "Empty response text.",
10
-        description: "Empty response from the API"
10
+        description: "Empty response from the API",
11 11
     },
12 12
     {
13 13
         id: "app.api.error",
14 14
         defaultMessage: "Unknown API error.",
15
-        description: "An unknown error occurred from the API"
16
-    }
15
+        description: "An unknown error occurred from the API",
16
+    },
17 17
 ];
18 18
 
19 19
 export default messages;

+ 10
- 10
app/locales/messagesDescriptors/common.js View File

@@ -2,52 +2,52 @@ const messages = [
2 2
     {
3 3
         id: "app.common.close",
4 4
         defaultMessage: "Close",
5
-        description: "Close"
5
+        description: "Close",
6 6
     },
7 7
     {
8 8
         id: "app.common.cancel",
9 9
         description: "Cancel",
10
-        defaultMessage: "Cancel"
10
+        defaultMessage: "Cancel",
11 11
     },
12 12
     {
13 13
         id: "app.common.go",
14 14
         description: "Go",
15
-        defaultMessage: "Go"
15
+        defaultMessage: "Go",
16 16
     },
17 17
     {
18 18
         id: "app.common.art",
19 19
         description: "Art",
20
-        defaultMessage: "Art"
20
+        defaultMessage: "Art",
21 21
     },
22 22
     {
23 23
         id: "app.common.artist",
24 24
         description: "Artist",
25
-        defaultMessage: "{itemCount, plural, one {artist} other {artists}}"
25
+        defaultMessage: "{itemCount, plural, one {artist} other {artists}}",
26 26
     },
27 27
     {
28 28
         id: "app.common.album",
29 29
         description: "Album",
30
-        defaultMessage: "{itemCount, plural, one {album} other {albums}}"
30
+        defaultMessage: "{itemCount, plural, one {album} other {albums}}",
31 31
     },
32 32
     {
33 33
         id: "app.common.track",
34 34
         description: "Track",
35
-        defaultMessage: "{itemCount, plural, one {track} other {tracks}}"
35
+        defaultMessage: "{itemCount, plural, one {track} other {tracks}}",
36 36
     },
37 37
     {
38 38
         id: "app.common.loading",
39 39
         description: "Loading indicator",
40
-        defaultMessage: "Loading…"
40
+        defaultMessage: "Loading…",
41 41
     },
42 42
     {
43 43
         id: "app.common.play",
44 44
         description: "Play icon description",
45
-        defaultMessage: "Play"
45
+        defaultMessage: "Play",
46 46
     },
47 47
     {
48 48
         id: "app.common.pause",
49 49
         description: "Pause icon description",
50
-        defaultMessage: "Pause"
50
+        defaultMessage: "Pause",
51 51
     },
52 52
 ];
53 53
 

+ 2
- 2
app/locales/messagesDescriptors/elements/FilterBar.js View File

@@ -2,12 +2,12 @@ const messages = [
2 2
     {
3 3
         id: "app.filter.filter",
4 4
         defaultMessage: "Filter…",
5
-        description: "Filtering input placeholder"
5
+        description: "Filtering input placeholder",
6 6
     },
7 7
     {
8 8
         id: "app.filter.whatAreWeListeningToToday",
9 9
         description: "Description for the filter bar",
10
-        defaultMessage: "What are we listening to today?"
10
+        defaultMessage: "What are we listening to today?",
11 11
     },
12 12
 ];
13 13
 

+ 6
- 6
app/locales/messagesDescriptors/elements/Pagination.js View File

@@ -2,28 +2,28 @@ const messages = [
2 2
     {
3 3
         id: "app.pagination.goToPage",
4 4
         defaultMessage: "<span class=\"sr-only\">Go to page </span>{pageNumber}",
5
-        description: "Link content to go to page N. span is here for screen-readers"
5
+        description: "Link content to go to page N. span is here for screen-readers",
6 6
     },
7 7
     {
8 8
         id: "app.pagination.goToPageWithoutMarkup",
9 9
         defaultMessage: "Go to page {pageNumber}",
10
-        description: "Link title to go to page N"
10
+        description: "Link title to go to page N",
11 11
     },
12 12
     {
13 13
         id: "app.pagination.pageNavigation",
14 14
         defaultMessage: "Page navigation",
15
-        description: "ARIA label for the nav block containing pagination"
15
+        description: "ARIA label for the nav block containing pagination",
16 16
     },
17 17
     {
18 18
         id: "app.pagination.pageToGoTo",
19 19
         description: "Title of the pagination modal",
20
-        defaultMessage: "Page to go to?"
20
+        defaultMessage: "Page to go to?",
21 21
     },
22 22
     {
23 23
         id: "app.pagination.current",
24 24
         description: "Current (page)",
25
-        defaultMessage: "current"
26
-    }
25
+        defaultMessage: "current",
26
+    },
27 27
 ];
28 28
 
29 29
 export default messages;

+ 8
- 8
app/locales/messagesDescriptors/elements/WebPlayer.js View File

@@ -2,38 +2,38 @@ const messages = [
2 2
     {
3 3
         id: "app.webplayer.by",
4 4
         defaultMessage: "by",
5
-        description: "Artist affiliation of a song"
5
+        description: "Artist affiliation of a song",
6 6
     },
7 7
     {
8 8
         id: "app.webplayer.previous",
9 9
         defaultMessage: "Previous",
10
-        description: "Previous button description"
10
+        description: "Previous button description",
11 11
     },
12 12
     {
13 13
         id: "app.webplayer.next",
14 14
         defaultMessage: "Next",
15
-        description: "Next button description"
15
+        description: "Next button description",
16 16
     },
17 17
     {
18 18
         id: "app.webplayer.volume",
19 19
         defaultMessage: "Volume",
20
-        description: "Volume button description"
20
+        description: "Volume button description",
21 21
     },
22 22
     {
23 23
         id: "app.webplayer.repeat",
24 24
         defaultMessage: "Repeat",
25
-        description: "Repeat button description"
25
+        description: "Repeat button description",
26 26
     },
27 27
     {
28 28
         id: "app.webplayer.random",
29 29
         defaultMessage: "Random",
30
-        description: "Random button description"
30
+        description: "Random button description",
31 31
     },
32 32
     {
33 33
         id: "app.webplayer.playlist",
34 34
         defaultMessage: "Playlist",
35
-        description: "Playlist button description"
36
-    }
35
+        description: "Playlist button description",
36
+    },
37 37
 ];
38 38
 
39 39
 export default messages;

+ 3
- 3
app/locales/messagesDescriptors/grid.js View File

@@ -2,13 +2,13 @@ const messages = [
2 2
     {
3 3
         id: "app.grid.goToArtistPage",
4 4
         defaultMessage: "Go to artist page",
5
-        description: "Artist thumbnail link title"
5
+        description: "Artist thumbnail link title",
6 6
     },
7 7
     {
8 8
         id: "app.grid.goToAlbumPage",
9 9
         defaultMessage: "Go to album page",
10
-        description: "Album thumbnail link title"
11
-    }
10
+        description: "Album thumbnail link title",
11
+    },
12 12
 ];
13 13
 
14 14
 export default messages;

+ 11
- 11
app/locales/messagesDescriptors/layouts/Sidebar.js View File

@@ -2,53 +2,53 @@ const messages = [
2 2
     {
3 3
         id: "app.sidebarLayout.mainNavigationMenu",
4 4
         description: "ARIA label for the main navigation menu",
5
-        defaultMessage: "Main navigation menu"
5
+        defaultMessage: "Main navigation menu",
6 6
     },
7 7
     {
8 8
         id: "app.sidebarLayout.home",
9 9
         description: "Home",
10
-        defaultMessage: "Home"
10
+        defaultMessage: "Home",
11 11
     },
12 12
     {
13 13
         id: "app.sidebarLayout.settings",
14 14
         description: "Settings",
15
-        defaultMessage: "Settings"
15
+        defaultMessage: "Settings",
16 16
     },
17 17
     {
18 18
         id: "app.sidebarLayout.logout",
19 19
         description: "Logout",
20
-        defaultMessage: "Logout"
20
+        defaultMessage: "Logout",
21 21
     },
22 22
     {
23 23
         id: "app.sidebarLayout.discover",
24 24
         description: "Discover",
25
-        defaultMessage: "Discover"
25
+        defaultMessage: "Discover",
26 26
     },
27 27
     {
28 28
         id: "app.sidebarLayout.browse",
29 29
         description: "Browse",
30
-        defaultMessage: "Browse"
30
+        defaultMessage: "Browse",
31 31
     },
32 32
     {
33 33
         id: "app.sidebarLayout.browseArtists",
34 34
         description: "Browse artists",
35
-        defaultMessage: "Browse artists"
35
+        defaultMessage: "Browse artists",
36 36
     },
37 37
     {
38 38
         id: "app.sidebarLayout.browseAlbums",
39 39
         description: "Browse albums",
40
-        defaultMessage: "Browse albums"
40
+        defaultMessage: "Browse albums",
41 41
     },
42 42
     {
43 43
         id: "app.sidebarLayout.browseSongs",
44 44
         description: "Browse songs",
45
-        defaultMessage: "Browse songs"
45
+        defaultMessage: "Browse songs",
46 46
     },
47 47
     {
48 48
         id: "app.sidebarLayout.toggleNavigation",
49 49
         description: "Screen reader description of toggle navigation button",
50
-        defaultMessage: "Toggle navigation"
51
-    }
50
+        defaultMessage: "Toggle navigation",
51
+    },
52 52
 ];
53 53
 
54 54
 export default messages;

+ 9
- 9
app/middleware/api.js View File

@@ -29,7 +29,7 @@ class APIError extends Error {}
29 29
  * @param   response    A XHR response object.
30 30
  * @return  The response or a rejected Promise if the check failed.
31 31
  */
32
-function _checkHTTPStatus (response) {
32
+function _checkHTTPStatus(response) {
33 33
     if (response.status >= 200 && response.status < 300) {
34 34
         return response;
35 35
     } else {
@@ -44,17 +44,17 @@ function _checkHTTPStatus (response) {
44 44
  * @param   responseText    The text from the API response.
45 45
  * @return  The response as a JS object or a rejected Promise on error.
46 46
  */
47
-function _parseToJSON (responseText) {
47
+function _parseToJSON(responseText) {
48 48
     let x2js = new X2JS({
49 49
         attributePrefix: "",  // No prefix for attributes
50
-        keepCData: false  // Do not store __cdata and toString functions
50
+        keepCData: false,  // Do not store __cdata and toString functions
51 51
     });
52 52
     if (responseText) {
53 53
         return x2js.xml_str2json(responseText).root;
54 54
     }
55 55
     return Promise.reject(new i18nRecord({
56 56
         id: "app.api.invalidResponse",
57
-        values: {}
57
+        values: {},
58 58
     }));
59 59
 }
60 60
 
@@ -65,14 +65,14 @@ function _parseToJSON (responseText) {
65 65
  * @param   jsonData  A JS object representing the API response.
66 66
  * @return  The input data or a rejected Promise if errors are present.
67 67
  */
68
-function _checkAPIErrors (jsonData) {
68
+function _checkAPIErrors(jsonData) {
69 69
     if (jsonData.error) {
70 70
         return Promise.reject(jsonData.error);
71 71
     } else if (!jsonData) {
72 72
         // No data returned
73 73
         return Promise.reject(new i18nRecord({
74 74
             id: "app.api.emptyResponse",
75
-            values: {}
75
+            values: {},
76 76
         }));
77 77
     }
78 78
     return jsonData;
@@ -85,7 +85,7 @@ function _checkAPIErrors (jsonData) {
85 85
  * @param   jsonData    A JS object representing the API response.
86 86
  * @return  A fixed JS object.
87 87
  */
88
-function _uglyFixes (jsonData) {
88
+function _uglyFixes(jsonData) {
89 89
     // Fix songs array
90 90
     let _uglyFixesSongs = function (songs) {
91 91
         return songs.map(function (song) {
@@ -201,7 +201,7 @@ function _uglyFixes (jsonData) {
201 201
  *
202 202
  * @return  A fetching Promise.
203 203
  */
204
-function doAPICall (endpoint, action, auth, username, extraParams) {
204
+function doAPICall(endpoint, action, auth, username, extraParams) {
205 205
     // Translate the API action to real API action
206 206
     const APIAction = extraParams.filter ? action.rstrip("s") : action;
207 207
     // Set base params
@@ -209,7 +209,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
209 209
         version: API_VERSION,
210 210
         action: APIAction,
211 211
         auth: auth,
212
-        user: username
212
+        user: username,
213 213
     };
214 214
     // Extend with extraParams
215 215
     const params = Object.assign({}, baseParams, extraParams);

+ 3
- 3
app/models/api.js View File

@@ -14,15 +14,15 @@ export const song = new Schema("song");  /** Song schema */
14 14
 // Explicit relations between them
15 15
 artist.define({  // Artist has albums and songs (tracks)
16 16
     albums: arrayOf(album),
17
-    songs: arrayOf(song)
17
+    songs: arrayOf(song),
18 18
 });
19 19
 
20 20
 album.define({  // Album has artist, tracks and tags
21 21
     artist: artist,
22
-    tracks: arrayOf(song)
22
+    tracks: arrayOf(song),
23 23
 });
24 24
 
25 25
 song.define({  // Track has artist and album
26 26
     artist: artist,
27
-    album: album
27
+    album: album,
28 28
 });

+ 2
- 2
app/models/auth.js View File

@@ -9,7 +9,7 @@ import Immutable from "immutable";
9 9
 /** Record to store token parameters */
10 10
 export const tokenRecord = Immutable.Record({
11 11
     token: null,  /** Token string */
12
-    expires: null  /** Token expiration date */
12
+    expires: null,  /** Token expiration date */
13 13
 });
14 14
 
15 15
 
@@ -23,5 +23,5 @@ export const stateRecord = new Immutable.Record({
23 23
     isAuthenticating: false,  /** Whether authentication is in progress or not */
24 24
     error: null,  /** An error string */
25 25
     info: null,  /** An info string */
26
-    timerID: null  /** Timer ID for setInterval calls to revive API session */
26
+    timerID: null,  /** Timer ID for setInterval calls to revive API session */
27 27
 });

+ 3
- 3
app/models/entities.js View File

@@ -12,11 +12,11 @@ export const stateRecord = new Immutable.Record({
12 12
     refCounts: new Immutable.Map({
13 13
         album: new Immutable.Map(),
14 14
         artist: new Immutable.Map(),
15
-        song: new Immutable.Map()
15
+        song: new Immutable.Map(),
16 16
     }),  /** Map of id => reference count for each object type (garbage collection) */
17 17
     entities: new Immutable.Map({
18 18
         album: new Immutable.Map(),
19 19
         artist: new Immutable.Map(),
20
-        song: new Immutable.Map()
21
-    })  /** Map of id => entity for each object type */
20
+        song: new Immutable.Map(),
21
+    }),  /** Map of id => entity for each object type */
22 22
 });

+ 1
- 1
app/models/i18n.js View File

@@ -8,5 +8,5 @@ import Immutable from "immutable";
8 8
 /** i18n record for passing errors to be localized from actions to components */
9 9
 export const i18nRecord = new Immutable.Record({
10 10
     id: null,  /** Translation message id */
11
-    values: new Immutable.Map()  /** Values to pass to formatMessage */
11
+    values: new Immutable.Map(),  /** Values to pass to formatMessage */
12 12
 });

+ 1
- 1
app/models/paginated.js View File

@@ -6,5 +6,5 @@ export const stateRecord = new Immutable.Record({
6 6
     type: null,  /** Type of the paginated entries */
7 7
     result: new Immutable.List(),  /** List of IDs of the resulting entries, maps to the entities store */
8 8
     currentPage: 1,  /** Number of current page */
9
-    nPages: 1  /** Total number of page in this batch */
9
+    nPages: 1,  /** Total number of page in this batch */
10 10
 });

+ 1
- 1
app/models/webplayer.js View File

@@ -14,5 +14,5 @@ export const stateRecord = new Immutable.Record({
14 14
     isMute: false,  /** Whether sound is muted or not */
15 15
     volume: 100,  /** Current volume, between 0 and 100 */
16 16
     currentIndex: 0,  /** Current index in the playlist */
17
-    playlist: new Immutable.List()  /** List of songs IDs, references songs in the entities store */
17
+    playlist: new Immutable.List(),  /** List of songs IDs, references songs in the entities store */
18 18
 });

+ 9
- 9
app/reducers/auth.js View File

@@ -68,8 +68,8 @@ export default createReducer(initialState, {
68 68
             isAuthenticating: true,
69 69
             info: new i18nRecord({
70 70
                 id: "app.login.connecting",
71
-                values: {}
72
-            })
71
+                values: {},
72
+            }),
73 73
         });
74 74
     },
75 75
     [LOGIN_USER_SUCCESS]: (state, payload) => {
@@ -81,28 +81,28 @@ export default createReducer(initialState, {
81 81
             "rememberMe": payload.rememberMe,
82 82
             "info": new i18nRecord({
83 83
                 id: "app.login.success",
84
-                values: {username: payload.username}
84
+                values: {username: payload.username},
85 85
             }),
86
-            "timerID": payload.timerID
86
+            "timerID": payload.timerID,
87 87
         });
88 88
     },
89 89
     [LOGIN_USER_FAILURE]: (state, payload) => {
90 90
         return new stateRecord({
91
-            "error": payload.error
91
+            "error": payload.error,
92 92
         });
93 93
     },
94 94
     [LOGIN_USER_EXPIRED]: (state, payload) => {
95 95
         return new stateRecord({
96 96
             "isAuthenticated": false,
97
-            "error": payload.error
97
+            "error": payload.error,
98 98
         });
99 99
     },
100 100
     [LOGOUT_USER]: () => {
101 101
         return new stateRecord({
102 102
             info: new i18nRecord({
103 103
                 id: "app.login.byebye",
104
-                values: {}
105
-            })
104
+                values: {},
105
+            }),
106 106
         });
107
-    }
107
+    },
108 108
 });

+ 12
- 6
app/reducers/entities.js View File

@@ -18,7 +18,7 @@ import {
18 18
     PUSH_ENTITIES,
19 19
     INCREMENT_REFCOUNT,
20 20
     DECREMENT_REFCOUNT,
21
-    INVALIDATE_STORE
21
+    INVALIDATE_STORE,
22 22
 } from "../actions";
23 23
 
24 24
 
@@ -171,9 +171,11 @@ export default createReducer(initialState, {
171 171
 
172 172
         // Increment reference counter
173 173
         payload.refCountType.forEach(function (itemName) {
174
-            newState.getIn(["entities", itemName]).forEach(function (entity, id) {
174
+            const entities = payload.entities[itemName];
175
+            for (let id in entities) {
176
+                const entity = newState.getIn(["entities", itemName, id]);
175 177
                 newState = updateEntityRefCount(newState, itemName, id, entity, 1);
176
-            });
178
+            }
177 179
         });
178 180
 
179 181
         return newState;
@@ -183,7 +185,9 @@ export default createReducer(initialState, {
183 185
 
184 186
         // Increment reference counter
185 187
         for (let itemName in payload.entities) {
186
-            newState.getIn(["entities", itemName]).forEach(function (entity, id) {
188
+            const entities = payload.entities[itemName];
189
+            entities.forEach(function (id) {
190
+                const entity = newState.getIn(["entities", itemName, id]);
187 191
                 newState = updateEntityRefCount(newState, itemName, id, entity, 1);
188 192
             });
189 193
         }
@@ -195,7 +199,9 @@ export default createReducer(initialState, {
195 199
 
196 200
         // Decrement reference counter
197 201
         for (let itemName in payload.entities) {
198
-            newState.getIn(["entities", itemName]).forEach(function (entity, id) {
202
+            const entities = payload.entities[itemName];
203
+            entities.forEach(function (id) {
204
+                const entity = newState.getIn(["entities", itemName, id]);
199 205
                 newState = updateEntityRefCount(newState, itemName, id, entity, -1);
200 206
             });
201 207
         }
@@ -207,5 +213,5 @@ export default createReducer(initialState, {
207 213
     },
208 214
     [INVALIDATE_STORE]: () => {
209 215
         return new stateRecord();
210
-    }
216
+    },
211 217
 });

+ 2
- 2
app/reducers/index.js View File

@@ -19,7 +19,7 @@ import * as ActionTypes from "../actions";
19 19
 const paginated = paginatedMaker([
20 20
     ActionTypes.API_REQUEST,
21 21
     ActionTypes.API_SUCCESS,
22
-    ActionTypes.API_FAILURE
22
+    ActionTypes.API_FAILURE,
23 23
 ]);
24 24
 
25 25
 // Export the combined reducers
@@ -28,5 +28,5 @@ export default combineReducers({
28 28
     auth,
29 29
     entities,
30 30
     paginated,
31
-    webplayer
31
+    webplayer,
32 32
 });

+ 3
- 3
app/reducers/paginated.js View File

@@ -12,7 +12,7 @@ import { createReducer } from "../utils";
12 12
 import { stateRecord } from "../models/paginated";
13 13
 
14 14
 // Actions
15
-import { CLEAR_RESULTS, INVALIDATE_STORE } from "../actions";
15
+import { CLEAR_PAGINATED_RESULTS, INVALIDATE_STORE } from "../actions";
16 16
 
17 17
 
18 18
 /** Initial state of the reducer */
@@ -50,12 +50,12 @@ export default function paginated(types) {
50 50
         [failureType]: (state) => {
51 51
             return state;
52 52
         },
53
-        [CLEAR_RESULTS]: (state) => {
53
+        [CLEAR_PAGINATED_RESULTS]: (state) => {
54 54
             return state.set("result", new Immutable.List());
55 55
         },
56 56
         [INVALIDATE_STORE]: () => {
57 57
             // Reset state on invalidation
58 58
             return new stateRecord();
59
-        }
59
+        },
60 60
     });
61 61
 }

+ 84
- 13
app/reducers/webplayer.js View File

@@ -1,16 +1,32 @@
1
-// TODO: This is a WIP
1
+/**
2
+ * This implements the webplayer reducers.
3
+ */
4
+
5
+// NPM imports
2 6
 import Immutable from "immutable";
3 7
 
8
+// Local imports
9
+import { createReducer } from "../utils";
10
+
11
+// Models
12
+import { stateRecord } from "../models/webplayer";
13
+
14
+// Actions
4 15
 import {
5
-    PUSH_PLAYLIST,
6
-    CHANGE_TRACK,
7 16
     PLAY_PAUSE,
17
+    STOP_PLAYBACK,
18
+    SET_PLAYLIST,
19
+    PUSH_SONG,
20
+    POP_SONG,
21
+    JUMP_TO_SONG,
22
+    PLAY_PREVIOUS,
23
+    PLAY_NEXT,
8 24
     TOGGLE_RANDOM,
9 25
     TOGGLE_REPEAT,
10 26
     TOGGLE_MUTE,
27
+    SET_VOLUME,
11 28
     INVALIDATE_STORE } from "../actions";
12
-import { createReducer } from "../utils";
13
-import { stateRecord } from "../models/webplayer";
29
+
14 30
 
15 31
 /**
16 32
  * Initial state
@@ -19,28 +35,80 @@ import { stateRecord } from "../models/webplayer";
19 35
 var initialState = new stateRecord();
20 36
 
21 37
 
38
+/**
39
+ * Helper functions
40
+ */
41
+
42
+/**
43
+ * Stop playback in reducer helper.
44
+ *
45
+ * @param   state   Current state to update.
46
+ */
47
+function stopPlayback(state) {
48
+    return (
49
+        state
50
+        .set("isPlaying", false)
51
+        .set("currentIndex", 0)
52
+        .set("playlist", new Immutable.List())
53
+    );
54
+}
55
+
56
+
22 57
 /**
23 58
  * Reducers
24 59
  */
25 60
 
26 61
 export default createReducer(initialState, {
27 62
     [PLAY_PAUSE]: (state, payload) => {
63
+        // Force play or pause
28 64
         return state.set("isPlaying", payload.isPlaying);
29 65
     },
30
-    [CHANGE_TRACK]: (state, payload) => {
31
-        return state.set("currentIndex", payload.index);
66
+    [STOP_PLAYBACK]: (state) => {
67
+        // Clear the playlist
68
+        return stopPlayback(state);
32 69
     },
33
-    [PUSH_PLAYLIST]: (state, payload) => {
70
+    [SET_PLAYLIST]: (state, payload) => {
71
+        // Set current playlist, reset playlist index
34 72
         return (
35 73
             state
36 74
             .set("playlist", new Immutable.List(payload.playlist))
37
-            .setIn(["entities", "artists"], new Immutable.Map(payload.artists))
38
-            .setIn(["entities", "albums"], new Immutable.Map(payload.albums))
39
-            .setIn(["entities", "tracks"], new Immutable.Map(payload.tracks))
40 75
             .set("currentIndex", 0)
41
-            .set("isPlaying", true)
42 76
         );
43 77
     },
78
+    [PUSH_SONG]: (state, payload) => {
79
+        // Push song to playlist
80
+        const newPlaylist = state.get("playlist").insert(payload.index, payload.song);
81
+        return state.set("playlist", newPlaylist);
82
+    },
83
+    [POP_SONG]: (state, payload) => {
84
+        // Pop song from playlist
85
+        return state.deleteIn(["playlist", payload.index]);
86
+    },
87
+    [JUMP_TO_SONG]: (state, payload) => {
88
+        // Set current index
89
+        const newCurrentIndex = state.get("playlist").findKey(x => x == payload.song);
90
+        return state.set("currentIndex", newCurrentIndex);
91
+    },
92
+    [PLAY_PREVIOUS]: (state) => {
93
+        const newIndex = state.get("currentIndex") - 1;
94
+        if (newIndex < 0) {
95
+            // If there is an overlow on the left of the playlist, just stop
96
+            // playback
97
+            return stopPlayback(state);
98
+        } else {
99
+            return state.set("currentIndex", newIndex);
100
+        }
101
+    },
102
+    [PLAY_NEXT]: (state) => {
103
+        const newIndex = state.get("currentIndex") + 1;
104
+        if (newIndex > state.get("playlist").size) {
105
+            // If there is an overflow, just stop playback
106
+            return stopPlayback(state);
107
+        } else {
108
+            // Else, play next item
109
+            return state.set("currentIndex", newIndex);
110
+        }
111
+    },
44 112
     [TOGGLE_RANDOM]: (state) => {
45 113
         return state.set("isRandom", !state.get("isRandom"));
46 114
     },
@@ -50,7 +118,10 @@ export default createReducer(initialState, {
50 118
     [TOGGLE_MUTE]: (state) => {
51 119
         return state.set("isMute", !state.get("isMute"));
52 120
     },
121
+    [SET_VOLUME]: (state, payload) => {
122
+        return state.set("volume", payload.volume);
123
+    },
53 124
     [INVALIDATE_STORE]: () => {
54 125
         return new stateRecord();
55
-    }
126
+    },
56 127
 });

+ 2
- 2
app/utils/ampache.js View File

@@ -15,7 +15,7 @@ import jsSHA from "jssha";
15 15
  * @remark  This builds an HMAC as expected by Ampache API, which is not a
16 16
  *          standard HMAC.
17 17
  */
18
-export function buildHMAC (password) {
18
+export function buildHMAC(password) {
19 19
     const time = Math.floor(Date.now() / 1000);
20 20
 
21 21
     let shaObj = new jsSHA("SHA-256", "TEXT");
@@ -27,6 +27,6 @@ export function buildHMAC (password) {
27 27
 
28 28
     return {
29 29
         time: time,
30
-        passphrase: shaObj.getHash("HEX")
30
+        passphrase: shaObj.getHash("HEX"),
31 31
     };
32 32
 }

+ 1
- 1
app/utils/immutable.js View File

@@ -10,7 +10,7 @@
10 10
  * @param   b   Second Immutable object.
11 11
  * @returns     An Immutable object equal to a except for the items in b.
12 12
  */
13
-export function immutableDiff (a, b) {
13
+export function immutableDiff(a, b) {
14 14
     return a.filter(function (i) {
15 15
         return b.indexOf(i) < 0;
16 16
     });

+ 1
- 1
app/utils/locale.js View File

@@ -6,7 +6,7 @@ import { i18nRecord } from "../models/i18n";
6 6
 /**
7 7
  * Get the preferred locales from the browser, as an array sorted by preferences.
8 8
  */
9
-export function getBrowserLocales () {
9
+export function getBrowserLocales() {
10 10
     let langs = [];
11 11
 
12 12
     if (navigator.languages) {

+ 2
- 2
app/utils/misc.js View File

@@ -10,7 +10,7 @@
10 10
  * @return  Either NaN if the string was not a valid int representation, or the
11 11
  *          int.
12 12
  */
13
-export function filterInt (value) {
13
+export function filterInt(value) {
14 14
     if (/^(\-|\+)?([0-9]+|Infinity)$/.test(value)) {
15 15
         return Number(value);
16 16
     }
@@ -24,7 +24,7 @@ export function filterInt (value) {
24 24
  * @param   time    Length of the song in seconds.
25 25
  * @return  Formatted length as MM:SS.
26 26
  */
27
-export function formatLength (time) {
27
+export function formatLength(time) {
28 28
     const min = Math.floor(time / 60);
29 29
     let sec = (time - 60 * min);
30 30
     if (sec < 10) {

+ 3
- 3
app/utils/pagination.js View File

@@ -17,14 +17,14 @@ export function buildPaginationObject(location, currentPage, nPages, goToPageAct
17 17
     const buildLinkToPage = function (pageNumber) {
18 18
         return {
19 19
             pathname: location.pathname,
20
-            query: Object.assign({}, location.query, { page: pageNumber })
20
+            query: Object.assign({}, location.query, { page: pageNumber }),
21 21
         };
22 22
     };
23 23
     return {
24 24
         currentPage: currentPage,
25 25
         nPages: nPages,
26 26
         goToPage: pageNumber => goToPageAction(buildLinkToPage(pageNumber)),
27
-        buildLinkToPage: buildLinkToPage
27
+        buildLinkToPage: buildLinkToPage,
28 28
     };
29 29
 }
30 30
 
@@ -57,6 +57,6 @@ export function computePaginationBounds(currentPage, nPages, maxNumberPagesShown
57 57
 
58 58
     return {
59 59
         lowerLimit: lowerLimit,
60
-        upperLimit: upperLimit + 1  // +1 to ease iteration in for with <
60
+        upperLimit: upperLimit + 1,  // +1 to ease iteration in for with <
61 61
     };
62 62
 }

+ 2
- 2
app/utils/url.js View File

@@ -11,7 +11,7 @@
11 11
  *
12 12
  * @return  A string with the full URL with GET params.
13 13
  */
14
-export function assembleURLAndParams (endpoint, params) {
14
+export function assembleURLAndParams(endpoint, params) {
15 15
     let url = endpoint + "?";
16 16
     Object.keys(params).forEach(
17 17
         key => {
@@ -34,7 +34,7 @@ export function assembleURLAndParams (endpoint, params) {
34 34
  * @param   An URL
35 35
  * @return  The cleaned URL
36 36
  */
37
-export function cleanURL (endpoint) {
37
+export function cleanURL(endpoint) {
38 38
     if (
39 39
         !endpoint.startsWith("//") &&
40 40
             !endpoint.startsWith("http://") &&

+ 5
- 5
app/views/AlbumPage.jsx View File

@@ -10,16 +10,16 @@ import Album from "../components/Album";
10 10
 // TODO: AlbumPage should be scrolled ArtistPage
11 11
 
12 12
 export class AlbumPage extends Component {
13
-    componentWillMount () {
13
+    componentWillMount() {
14 14
         // Load the data
15 15
         this.props.actions.loadAlbums({
16 16
             pageNumber: 1,
17 17
             filter: this.props.params.id,
18
-            include: ["songs"]
18
+            include: ["songs"],
19 19
         });
20 20
     }
21 21
 
22
-    render () {
22
+    render() {
23 23
         if (this.props.album) {
24 24
             return (
25 25
                 <Album album={this.props.album} songs={this.props.songs} />
@@ -52,12 +52,12 @@ const mapStateToProps = (state, ownProps) => {
52 52
     }
53 53
     return {
54 54
         album: album,
55
-        songs: songs
55
+        songs: songs,
56 56
     };
57 57
 };
58 58
 
59 59
 const mapDispatchToProps = (dispatch) => ({
60
-    actions: bindActionCreators(actionCreators, dispatch)
60
+    actions: bindActionCreators(actionCreators, dispatch),
61 61
 });
62 62
 
63 63
 export default connect(mapStateToProps, mapDispatchToProps)(AlbumPage);

+ 7
- 7
app/views/AlbumsPage.jsx View File

@@ -25,13 +25,13 @@ const albumsMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
25 25
  * Albums page, grid layout of albums arts.
26 26
  */
27 27
 class AlbumsPageIntl extends Component {
28
-    componentWillMount () {
28
+    componentWillMount() {
29 29
         // Load the data for current page
30 30
         const currentPage = parseInt(this.props.location.query.page) || 1;
31 31
         this.props.actions.loadPaginatedAlbums({ pageNumber: currentPage });
32 32
     }
33 33
 
34
-    componentWillReceiveProps (nextProps) {
34
+    componentWillReceiveProps(nextProps) {
35 35
         // Load the data if page has changed
36 36
         const currentPage = parseInt(this.props.location.query.page) || 1;
37 37
         const nextPage = parseInt(nextProps.location.query.page) || 1;
@@ -40,12 +40,12 @@ class AlbumsPageIntl extends Component {
40 40
         }
41 41
     }
42 42
 
43
-    componentWillUnmount () {
43
+    componentWillUnmount() {
44 44
         // Unload data on page change
45
-        this.props.actions.clearResults();
45
+        this.props.actions.clearPaginatedResults();
46 46
     }
47 47
 
48
-    render () {
48
+    render() {
49 49
         const {formatMessage} = this.props.intl;
50 50
 
51 51
         const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
@@ -74,12 +74,12 @@ const mapStateToProps = (state) => {
74 74
         error: state.entities.error,
75 75
         albumsList: albumsList,
76 76
         currentPage: state.paginated.currentPage,
77
-        nPages: state.paginated.nPages
77
+        nPages: state.paginated.nPages,
78 78
     };
79 79
 };
80 80
 
81 81
 const mapDispatchToProps = (dispatch) => ({
82
-    actions: bindActionCreators(actionCreators, dispatch)
82
+    actions: bindActionCreators(actionCreators, dispatch),
83 83
 });
84 84
 
85 85
 export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl));

+ 8
- 8
app/views/ArtistPage.jsx View File

<
@@ -25,27 +25,27 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
25 25
  * Single artist page.
26 26
  */
27 27
 class ArtistPageIntl extends Component {
28
-    componentWillMount () {
28
+    componentWillMount() {
29 29
         // Load the data
30 30
         this.props.actions.loadArtist({
31 31
             filter: this.props.params.id,
32
-            include: ["albums", "songs"]
32
+            include: ["albums", "songs"],
33 33
         });
34 34
     }
35 35
 
36
-    componentWillUnmount () {
36
+    componentWillUnmount() {
37 37
         this.props.actions.decrementRefCount({
38
-            "artist": [this.props.artist.get("id")]
38
+            "artist": [this.props.artist.get("id")],
39 39
         });
40 40
     }
41 41
 
42
-    render () {
42
+    render() {
43 43
         const {formatMessage} = this.props.intl;
44 44
 
45 45
         const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
46 46
 
47 47
         return (
48
-            <Artist playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
48
+            <Artist playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
49 49
         );
50 50
     }
51 51
 }
@@ -82,12 +82,12 @@ const mapStateToProps = (state, ownProps) => {
82 82
         error: state.entities.error,
83 83
         artist: artist,
84 84
         albums: albums,
85
-        songs: songs
85
+        songs: songs,
86 86
     };
87 87
 };
88 88
 
89 89
 const mapDispatchToProps = (dispatch) => ({
90
-    actions: bindActionCreators(actionCreators, dispatch)
90
+    actions: bindActionCreators(actionCreators, dispatch),