├── .gitignore
├── LICENSE
├── README.md
├── demo.gif
├── dev
├── css
│ └── styles.css
├── fonts
│ ├── IcoMoon-Free.ttf
│ ├── Read Me.txt
│ ├── Reference.html
│ └── selection.json
└── js
│ ├── actions
│ ├── dataActions.js
│ ├── favoritesActions.js
│ ├── playerActions.js
│ ├── playlistActions.js
│ └── uiActions.js
│ ├── components
│ ├── Content.js
│ ├── ControlBar.js
│ ├── ControlBarButton.js
│ ├── Cover.js
│ ├── CurrentTabPicker.js
│ ├── CurrentTimeBar.js
│ ├── DurationBar.js
│ ├── Equalizer.js
│ ├── FavoritesItem.js
│ ├── Header.js
│ ├── Playlist.js
│ ├── PlaylistItem.js
│ ├── ProgressBar.js
│ ├── Tab.js
│ ├── Tooltip.js
│ ├── TracksList.js
│ ├── VolumeBar.js
│ ├── buttons
│ │ ├── NextButton.js
│ │ ├── PlayButton.js
│ │ ├── PrevButton.js
│ │ ├── RepeatButton.js
│ │ ├── ShuffleButton.js
│ │ └── SpeakerButton.js
│ └── sliders
│ │ ├── EqualizerVerticalSlider.js
│ │ ├── VerticalSlider.js
│ │ └── VerticalSliderItem.js
│ ├── constants
│ ├── dataConstants.js
│ ├── favoritesConstants.js
│ ├── playerConstants.js
│ ├── playlistConstants.js
│ └── uiConstants.js
│ ├── containers
│ ├── ActionBar.js
│ ├── App.js
│ ├── Artwork.js
│ ├── Playlist.js
│ ├── ProcessingBar.js
│ ├── SearchBar.js
│ ├── TimeBar.js
│ ├── TrackData.js
│ └── VoiceBar.js
│ ├── index.js
│ ├── reducers
│ ├── dataReducer.js
│ ├── factory.js
│ ├── favoritesReducer.js
│ ├── initialState.js
│ ├── playerReducer.js
│ ├── playlistReducer.js
│ ├── rootReducer.js
│ ├── store.js
│ └── uiReducer.js
│ └── utils
│ ├── CanvasSpectrum.js
│ ├── WebAudioAnalyzer.js
│ ├── dom.js
│ ├── format.js
│ ├── localStore.js
│ ├── playerAPI.js
│ ├── trackUtils.js
│ └── voiceCommands.js
├── gulpfile.js
├── index.html
├── package.json
└── public
├── css
└── styles.css
├── fonts
├── IcoMoon-Free.ttf
├── Read Me.txt
├── Reference.html
└── selection.json
├── index.html
└── js
├── all.js
├── bundle.min.js
└── vendor.min.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.idea
3 | /ReactPlayer.rar
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 abitlog
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react.js-voice-audio-player
2 |
3 | 
4 |
5 | A functional and lightweight react-redux audio player built on the top of the Soundcloud API. After the microphone button has been clicked you can use the player remotely. Just by using your voice.
6 |
7 | Check out at https://abitlog.github.io/react.js-voice-audio-player
8 |
9 | ### Some of advantages:
10 | 1. Player does not require a server, except if you'd like to use a voice control
11 | 2. Uses Soundcloud API to fetch the tracks
12 | 3. Uses local storage API to save the tracks you marked as favorites
13 | 4. Uses web audio API to perform spectrum visualization and filter frequencies
14 | 5. You can switch tracks back and forth, repeat them, shuffle, search for new ones either manually or by your voice
15 |
16 | ### The list of the voice commands:
17 | **"Switch"** - toggles the track's playback
18 | **"Play next track"** - plays the next track according to the current playing tab
19 | **"Play previous track"** - plays the previous track according to the current playing tab
20 | **"Repeat track"** - toggles the repeat switcher
21 | **"Search for"** - search the track/author/whatever. So, a voice command "Search for Vivaldi" will search for some Vivaldi's music
22 | **"Play playlist"** - starts playing the first track from the playlist
23 | **"Play favorites"** - starts playing the first track from the favorites
24 | **"Shuffle"** - shuffles the list according to the current tab
25 |
26 | ### To run a local node server:
27 | 1. clone the repo `git clone https://github.com/abitlog/react.js-voice-audio-player.git && cd react.js-voice-audio-player`
28 | 2. `npm install`
29 | 3. gulp v4 has to be installed [locally and globally](https://www.liquidlight.co.uk/blog/article/how-do-i-update-to-gulp-4/)
30 | - `npm rm -g gulp`
31 | - `npm install -g gulp-cli`
32 | - `npm install 'gulpjs/gulp.git#4.0' --save-dev`
33 | 4. run `npm run dev` to get the app started or `set NODE_ENV=development& gulp dev` for windows
34 | 5. go to `http://localhost:3000`
35 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grobjin9/react.js-voice-audio-player/d57f06b0d7cfd3497e0b66c65775fa6cce4eb8f4/demo.gif
--------------------------------------------------------------------------------
/dev/css/styles.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | menu,
77 | nav,
78 | output,
79 | ruby,
80 | section,
81 | summary,
82 | time,
83 | mark,
84 | audio,
85 | video {
86 | margin: 0;
87 | padding: 0;
88 | border: 0;
89 | vertical-align: baseline;
90 | }
91 |
92 |
93 | /* HTML5 display-role reset for older browsers */
94 |
95 | article,
96 | aside,
97 | details,
98 | figcaption,
99 | figure,
100 | footer,
101 | header,
102 | menu,
103 | nav,
104 | section {
105 | display: block;
106 | }
107 |
108 | body {
109 | line-height: 1;
110 | }
111 |
112 | ol,
113 | ul {
114 | list-style: none;
115 | }
116 |
117 | blockquote,
118 | q {
119 | quotes: none;
120 | }
121 |
122 | blockquote:before,
123 | blockquote:after,
124 | q:before,
125 | q:after {
126 | content: '';
127 | content: none;
128 | }
129 |
130 | table {
131 | border-collapse: collapse;
132 | border-spacing: 0;
133 | }
134 |
135 | * {
136 | -webkit-font-smoothing: antialiased;
137 | }
138 |
139 | @font-face {
140 | font-family: 'IcoMoon-Free';
141 | src: url('../fonts/IcoMoon-Free.ttf') format('truetype');
142 | font-weight: normal;
143 | font-style: normal;
144 | }
145 |
146 | .icon {
147 | color: #666666;
148 | font-size: 1.1em;
149 | /* use !important to prevent issues with browser extensions that change fonts */
150 | font-family: 'IcoMoon-Free' !important;
151 | speak: none;
152 | font-style: normal;
153 | font-weight: normal;
154 | font-variant: normal;
155 | text-transform: none;
156 | line-height: 1;
157 | display: inline-block;
158 | cursor: pointer;
159 | /* Enable Ligatures ================ */
160 | letter-spacing: 0;
161 | -webkit-font-feature-settings: "liga";
162 | -ms-font-feature-settings: "liga" 1;
163 | -o-font-feature-settings: "liga";
164 | font-feature-settings: "liga";
165 | /* Better Font Rendering =========== */
166 | -webkit-font-smoothing: antialiased;
167 | -moz-osx-font-smoothing: grayscale;
168 | }
169 |
170 | body {
171 | box-sizing: border-box;
172 | -webkit-font-smoothing: subpixel-antialiased;
173 | }
174 |
175 | li,
176 | div,
177 | img {
178 | -webkit-user-select: none;
179 | -moz-user-select: none;
180 | -ms-user-select: none;
181 | user-select: none;
182 | }
183 |
184 | *:focus,
185 | *:active {
186 | outline: 0;
187 | }
188 |
189 | .container {
190 | margin-right: auto;
191 | margin-left: auto;
192 | padding-left: 6px;
193 | padding-right: 6px;
194 | }
195 |
196 | .container:before,
197 | .container:after {
198 | content: " ";
199 | display: table;
200 | }
201 |
202 | .container:after {
203 | clear: both;
204 | }
205 |
206 | #player {
207 | position: relative;
208 | width: 700px;
209 | margin: 30px auto 0;
210 | border-radius: 4px;
211 | background-color: white;
212 | overflow: hidden;
213 | z-index: 2;
214 | border: 1px solid gray;
215 | }
216 |
217 | #player__visual-bar {
218 | position: relative;
219 | width: 100%;
220 | height: 340px;
221 | overflow: hidden;
222 | z-index: 1;
223 | }
224 |
225 | .artwork {
226 | float: left;
227 | position: relative;
228 | width: 100%;
229 | height: 100%;
230 | box-sizing: border-box;
231 | padding: 10px 40px 30px;
232 | }
233 |
234 | .artwork__img {
235 | width: 100%;
236 | max-width: 100%;
237 | height: 100%;
238 | border-radius: 3px;
239 | }
240 |
241 | .visual-bar-right {
242 | float: left;
243 | width: 50%;
244 | }
245 |
246 | .visual-bar-right-header {
247 | width: 100%;
248 | height: 100%;
249 | box-sizing: border-box;;
250 | padding: 15px 30px;
251 | }
252 |
253 | .visual-bar-left-header {
254 | width: 100%;
255 | box-sizing: border-box;;
256 | padding: 15px 30px;
257 | }
258 |
259 | .track-data {
260 | width: 85%;
261 | float: left;
262 |
263 | -webkit-box-sizing: border-box;
264 | -moz-box-sizing: border-box;
265 | box-sizing: border-box;
266 | }
267 |
268 | .voice-bar {
269 | width: 15%;
270 | height: 100%;
271 | float: right;
272 | text-align: right;
273 |
274 |
275 | -webkit-box-sizing: border-box;
276 | -moz-box-sizing: border-box;
277 | box-sizing: border-box;
278 | }
279 |
280 | .voice-bar__controller {
281 | width: 50%;
282 | text-align: center;
283 | transition: color .5s;
284 | }
285 |
286 | .voice-bar__controller:hover {
287 | color: #3cd2ce;
288 | }
289 |
290 | .voice-bar__controller::after {
291 | content: '\e91e';
292 | width: 100%;
293 | height: 100%;
294 | }
295 |
296 | .track-data__title {
297 | font-family: 'Hind Siliguri', sans-serif;
298 | font-weight: bold;
299 | font-size: .9em;
300 | color: #5e5e5e;
301 | white-space: nowrap;
302 | overflow: hidden;
303 | transition: text-indent 1.5s;
304 | cursor: default;
305 | }
306 |
307 | .track-data__username {
308 | font-family: 'Open Sans Condensed', sans-serif;
309 | font-size: 0.85em;
310 | color: #5e5e5e;
311 | padding-top: 4px;
312 | }
313 |
314 | .search-bar {
315 | width: 75%;
316 | margin:0 auto;
317 | border: 1px solid rgba(0,0,0,.1);
318 | border-radius: 15px;
319 | }
320 |
321 | .search-bar__magnifier-sign {
322 | width: 10%;
323 | display: inline-block;
324 | font-size: 13px;
325 | text-align: right;
326 | cursor: default;
327 | }
328 |
329 | .search-bar__magnifier-sign::before {
330 | content: '\e986';
331 | }
332 |
333 | .search-bar__input {
334 | font-family: 'Open Sans Condensed',sans-serif;
335 | font-size: .9em;
336 | width: 80%;
337 | border-radius: 5px;
338 | border: 0;
339 | padding: 5px;
340 | box-sizing: border-box;
341 | }
342 |
343 | .hide {
344 | display: none !important;
345 | }
346 |
347 | .search-bar__x-sign {
348 | width: 10%;
349 | display: inline-block;
350 | font-size: 9px;
351 | transform: translate(4px, -2px);
352 | color: rgba(0, 0, 0, .2);
353 | cursor: default;
354 | }
355 |
356 | .search-bar__x-sign::before {
357 | content: '\ea0f';
358 | }
359 |
360 | #header {
361 | height: 60px;
362 | width: 100%;
363 | }
364 |
365 | .header__left {
366 | width: 50%;
367 | float: left;
368 | height: 100%;
369 | }
370 |
371 | .header__right {
372 | width: 50%;
373 | float: right;
374 | height: 100%;
375 | }
376 |
377 | .visual-bar-right-body {
378 | position: relative;
379 | clear: both;
380 | width: 100%;
381 | height: 100%;
382 | z-index: 1;
383 | }
384 |
385 | .track-list {
386 | position: relative;
387 | height: 229px;
388 | background-color: rgba(227, 227, 227, 0.4);
389 | box-sizing: border-box;;
390 | overflow: hidden;
391 | }
392 |
393 | .currentTrack {
394 | box-shadow: 0 0 5px rgba(60, 210, 206, 0.5);
395 | background-color: rgba(60, 210, 206, 0.4);
396 | }
397 |
398 | .playlist__item {
399 | font-family: 'Hind Siliguri', sans-serif;
400 | color: #555555;
401 | font-size: 0.78em;
402 | border-top: 1px solid rgba(227,227,227,.7);
403 | position: relative;
404 | padding: 11px 10px 11px 30px;
405 | -webkit-transition: all .4s;
406 | transition: all .4s;
407 | box-sizing: border-box;
408 | }
409 |
410 | .playlist__item:first-child {
411 | border-top: none;
412 | }
413 |
414 | .playlist__item:hover {
415 | cursor: default;
416 | box-shadow: 0 0 5px rgba(60, 210, 206, 0.5);
417 | background-color: rgba(60, 210, 206, 0.4);
418 | }
419 |
420 | .track-title {
421 | float: left;
422 | width: 87%;
423 | box-sizing: border-box;
424 | overflow: hidden;
425 | white-space: nowrap;
426 | transition: text-indent 2s;
427 | }
428 |
429 | .track-index {
430 | float: left;
431 | padding-right: 7px;
432 | width: 5%;
433 | font-size: 9px;
434 | color: #3cd2ce;
435 | font-weight: bold;
436 | box-sizing: border-box;
437 | }
438 |
439 | .track-star {
440 | padding: 0 4px;
441 | color: rgba(102, 102, 102, .4);
442 | }
443 |
444 | .track-unstar {
445 | padding: 0 4px;
446 | color: rgba(163, 12, 7, .4);
447 | }
448 |
449 | .track-star::after {
450 | content: '\ea0a';
451 | font-size: .8em;
452 | text-align: center;;
453 | box-sizing: border-box;
454 | transition: color .3s, content .5s;
455 | visibility: hidden;
456 | }
457 |
458 | .track-star__favorite::after {
459 | content: '\ea0f' !important;
460 | font-size: .8em !important;
461 | color: rgba(163, 12, 7, .7) !important;
462 | text-align: center;;
463 | box-sizing: border-box;
464 | transition: color .3s, content .5s;
465 | visibility: hidden;
466 | }
467 |
468 | .playlist__item:hover .track-star::after{
469 | visibility: visible;
470 | }
471 |
472 | .list-scroller {
473 | position: absolute;
474 | top: 0;
475 | right: 2px;
476 | width: 4px;
477 | height: 80px;
478 | background-color: rgba(85, 85, 85, 0.3);
479 | border-radius: 4px;
480 | cursor: default;
481 | z-index: 1000;
482 | -webkit-transition: background-color .4s, top .1s;
483 | transition: background-color .4s, top .1s;
484 | visibility: hidden;
485 | }
486 |
487 | .list-scroller:hover {
488 | background-color: rgba(85, 85, 85, 0.5);
489 | }
490 |
491 | .clearfix:before,
492 | .clearfix:after {
493 | content: "";
494 | display: table;
495 | }
496 |
497 | .clearfix:after {
498 | clear: both;
499 | }
500 |
501 | .clearfix {
502 | zoom: 1;
503 | }
504 |
505 | #control-bar {
506 | position: relative;
507 | clear: both;
508 | padding: 20px 30px;
509 | box-shadow: 0 -1px 20px rgba(0, 0, 0, 0.3);
510 | z-index: 1;
511 | }
512 |
513 | .control-bar__content {
514 | width: 100%;
515 | max-width: 100%;
516 | display: table;
517 | }
518 |
519 | .action-bar {
520 | display: table-cell;
521 | width: 15%;
522 | }
523 |
524 | .action-bar ul {
525 | width: 100%;
526 | display: table;
527 | }
528 |
529 | .action-bar li {
530 | display: table-cell;
531 | vertical-align: middle;
532 | text-align: center;
533 | width: 33%;
534 | }
535 |
536 | .action-bar li.prev::before {
537 | content: "\ea1f";
538 | }
539 |
540 | .action-bar li.stop::before {
541 | content: "\ea1d";
542 | }
543 |
544 | .action-bar li.play::before {
545 | content: "\ea1c";
546 | }
547 |
548 | .action-bar li.next::before {
549 | content: "\ea20";
550 | }
551 |
552 | .time-bar {
553 | display: table-cell;
554 | width: 60%;
555 | }
556 |
557 | .time-bar ul {
558 | width: 100%;
559 | display: table;
560 | }
561 |
562 | .time-bar li {
563 | display: table-cell;
564 | width: 80%;
565 | text-align: center;
566 | vertical-align: middle;
567 | }
568 |
569 | .time-bar li:first-child,
570 | .time-bar li:last-child {
571 | font-family: tahoma, arial, verdana, sans-serif, Lucida Sans;
572 | font-size: 0.65em;
573 | color: #555555;
574 | width: 10%;
575 | }
576 |
577 | .time-bar li:first-child {
578 | text-align: right;
579 | }
580 |
581 | .time-bar li:last-child {
582 | text-align: left;
583 | }
584 |
585 | .time-bar__progress-bar .progress-slider {
586 | width: 90%;
587 | }
588 |
589 | .progress-slider {
590 | display: inline-block;
591 | position: relative;
592 | height: 6px;
593 | cursor: pointer;
594 | box-shadow: 0 0 0 #000000, 0 0 0 #0d0d0d;
595 | background: #dddddd;
596 | border-radius: 4px;
597 | border: 0 solid rgba(0, 0, 0, 0);
598 | z-index: 10;
599 | }
600 |
601 | .progress-slider > .progress-slider__thumb {
602 | position: absolute;
603 | left: 0;
604 | box-shadow: 0 0 1px #000000, 0 0 1px #0d0d0d;
605 | border: 4px solid #ffffff;
606 | height: 5px;
607 | width: 5px;
608 | border-radius: 50%;
609 | background: #3cd2ce;
610 | cursor: pointer;
611 | margin-top: -3.5px;
612 | z-index: 100;
613 | }
614 |
615 | .progress-slider__timeCounter {
616 | position: absolute;
617 | height: 6px;
618 | width: 3px;
619 | left: 0;
620 | top: 0;
621 | background-color: rgb(60, 210, 206);
622 | border-radius: 4px;
623 | z-index: 5;
624 | }
625 |
626 | .processing-bar {
627 | display: table-cell;
628 | width: 25%;
629 | }
630 |
631 | .processing-bar ul {
632 | width: 100%;
633 | display: table;
634 | }
635 |
636 | .processing-bar li {
637 | position: relative;
638 | display: table-cell;
639 | width: 25%;
640 | text-align: center;
641 | vertical-align: middle;
642 | }
643 |
644 | .processing-bar li.volume-high::before {
645 | content: "\ea26";
646 | }
647 |
648 | .processing-bar li.volume-medium::before {
649 | content: "\ea27";
650 | }
651 |
652 | .processing-bar li.volume-low::before {
653 | content: "\ea28";
654 | }
655 |
656 | .processing-bar li.volume-mute::before {
657 | content: "\ea2a";
658 | }
659 |
660 | .processing-bar li.equalizer::before {
661 | content: "\e993";
662 | }
663 |
664 | #processing-bar__shuffle::before {
665 | content: "\ea30"
666 | }
667 |
668 | #repeat::before {
669 | content: "\ea2e";
670 | }
671 |
672 | .progress-slider__time-pointer {
673 | display: inline-block;
674 | position: absolute;
675 | background-color: black;
676 | width: 6px;
677 | height: 6px;
678 | }
679 |
680 | .player-tooltip {
681 | visibility: hidden;
682 | background-color: white;
683 | text-align: center;
684 |
685 | position: absolute;
686 | left: 50%;
687 | top: -78%;
688 | transform: translate(-50%, -100%);
689 |
690 | z-index: 1;
691 |
692 | cursor: default;
693 |
694 | transition: opacity .8s;
695 | }
696 |
697 | .player-tooltip--medium {
698 | padding: 10px;
699 | box-shadow: 0 6px 25px 1px rgba(0, 0, 0, 0.2);
700 | border-radius: 6px;
701 | }
702 |
703 | .player-tooltip--small {
704 | padding: 5px;
705 | box-shadow: 0 6px 25px 1px rgba(0, 0, 0, 0.2);
706 | border-radius: 6px;
707 | }
708 |
709 | .player-tooltip::after {
710 | content: '';
711 | position: absolute;
712 | top: 100%;
713 | left: 50%;
714 | }
715 |
716 | /*.player-tooltip_slider {*/
717 | /*width: 90px;*/
718 | /*}*/
719 |
720 |
721 | .vertical-slider-wrapper {
722 | display: inline-block;
723 | width: 100%;
724 | }
725 |
726 | .vertical-slider-wrapper__slider-wrapper {
727 | display: inline-block;
728 | width: 13px;
729 | }
730 |
731 | .vertical-slider-wrapper__title-wrapper {
732 | font-size: 0.6em;
733 | font-family: 'Hind Siliguri', sans-serif;
734 | text-align: center;
735 | font-weight: bold;
736 | margin-top: 5px;
737 | color: #bfbfbf;
738 | width: 100%;
739 | }
740 |
741 | /*.player-tooltip {*/
742 | /*width: 100%;*/
743 | /*}*/
744 |
745 | .player-tooltip--medium::after {
746 | border-left: 8px solid transparent;
747 | border-right: 8px solid transparent;
748 | border-top: 8px solid white;
749 | margin-left: -8px;
750 | }
751 |
752 | .player-tooltip--small::after {
753 | border-left: 6px solid transparent;
754 | border-right: 6px solid transparent;
755 | border-top: 6px solid white;
756 | margin-left: -6px;
757 | }
758 |
759 | .player-tooltip__title {
760 | font-family: 'Hind Siliguri', sans-serif;
761 | text-align: center;
762 | font-weight: bold;
763 | margin-top: 5px;
764 | color: #bfbfbf;
765 | width: 100%;
766 | }
767 |
768 | .player-tooltip__title--medium {
769 | font-size: 0.6em;
770 | }
771 |
772 | .player-tooltip__title--small {
773 | font-size: 0.4em;
774 | }
775 |
776 | .vertical-slider {
777 | display: inline-block;
778 | position: relative;
779 | height: 80px;
780 | width: 5px;
781 | box-shadow: 0 0 0 #000000, 0 0 0 #0d0d0d;
782 | background: #dddddd;
783 | border-radius: 4px;
784 | border: 0 solid rgba(0, 0, 0, 0);
785 | z-index: 10;
786 | }
787 |
788 | .vertical-slider__thumb {
789 | position: absolute;
790 | left: 0;
791 | top: 0;
792 | box-shadow: 0 0 1px #000000, 0 0 1px #0d0d0d;
793 | border: 3px solid #ffffff;
794 | height: 4px;
795 | width: 4px;
796 | border-radius: 50%;
797 | background: #3cd2ce;
798 | cursor: pointer;
799 | margin-left: -2px;
800 | z-index: 100;
801 | }
802 |
803 | .vertical-slider__timeCounter {
804 | position: absolute;
805 | height: 6px;
806 | width: 5px;
807 | left: 0;
808 | bottom: 0;
809 | background-color: rgb(60, 210, 206);
810 | border-radius: 4px;
811 | z-index: 5;
812 | }
813 |
814 | .activated {
815 | color: #3ccecf;
816 | z-index: 1000;
817 | }
818 |
819 | .vol-title {
820 | font-family: 'Hind Siliguri', sans-serif;
821 | font-weight: bold;
822 | margin-top: 5px;
823 | font-size: 0.6em;
824 | color: #bfbfbf;
825 | width: 100%;
826 | }
827 |
828 | .vol-title span {
829 | text-align: center;
830 | }
831 |
832 | .content-container {
833 | font-family: 'Hind Siliguri', sans-serif;
834 | color: #5e5e5e;
835 | margin: 0 auto;
836 | text-align: center;
837 | width: 60%;
838 | height: 70%;
839 | vertical-align: middle;
840 | }
841 |
842 | #content {
843 | position: relative;
844 | width: 100%;
845 | height: 263px;
846 | overflow: hidden;
847 | }
848 |
849 | .content__left {
850 | position: relative;
851 | float: left;
852 | width: 50%;
853 | height: 100%;
854 | }
855 |
856 | .content__right {
857 | position: relative;
858 | float: left;
859 | width: 50%;
860 | height: 100%;
861 | z-index: 1;
862 | }
863 |
864 | .action-text {
865 | font-size: 1.5em;
866 | }
867 |
868 | .file-b-lg {
869 | display: inline-block;
870 | font-size: 1.2em;
871 | padding: 7px 14px;
872 | border: 1px solid grey;
873 | border-radius: 3px;
874 | margin: 15px 0;
875 | cursor: pointer;
876 | }
877 |
878 |
879 | .react-spinner {
880 | position: absolute;
881 | width: 45px;
882 | height: 45px;
883 | top: 50%;
884 | left: 50%;
885 | }
886 |
887 | .react-spinner_bar {
888 | -webkit-animation: react-spinner_spin 1.2s linear infinite;
889 | -moz-animation: react-spinner_spin 1.2s linear infinite;
890 | animation: react-spinner_spin 1.2s linear infinite;
891 | border-radius: 5px;
892 | background-color: white;
893 | border: 1px solid rgba(53, 63, 77, 0.29);
894 | position: absolute;
895 | width: 20%;
896 | height: 7.8%;
897 | top: -3.9%;
898 | left: -10%;
899 | }
900 |
901 | #canvas {
902 | position: absolute;
903 | left: calc(50% - 260px/2);
904 | bottom: 0;
905 | text-align: center;
906 | }
907 |
908 | .control-bar__content li.icon {
909 | transition: color .3s, transform .6s;
910 | }
911 |
912 | .control-bar__content li.icon:hover {
913 | color: #3ccecf;
914 | }
915 |
916 | .playlist-menu {
917 | height: auto;
918 | width: 100%;
919 | box-sizing: border-box;
920 | }
921 |
922 | .playlist-menu input[type="radio"] {
923 | display:none;
924 | }
925 |
926 | .playlist-tab,
927 | .favorites-tab {
928 | clear: both;
929 | position: relative;
930 | }
931 |
932 | .playlist-menu__tab {
933 | font-family: 'Open Sans Condensed', sans-serif;
934 | font-size: .8em;
935 | display: block;
936 | width: 50%;
937 | float: left;
938 | padding: 10px;
939 | text-align: center;
940 | box-sizing: border-box;
941 | cursor: pointer;
942 | transition: color .3s;
943 | }
944 |
945 | .playlist-menu__tab span {
946 | position: relative;
947 | font-size: .6em;
948 | left: 2px;
949 | top: -5px;;
950 | }
951 |
952 | .playlist-menu input[type="radio"]:checked + label {
953 | font-size: .9em;
954 | color: #3cd2ce;
955 | background-color: rgba(227,227,227,.4);
956 | }
957 |
958 | .inactive{
959 | display: none;
960 | }
961 |
962 | .activatedBlock {
963 | opacity: 0;
964 | transition: opacity 1s;
965 | }
966 |
967 | /* *******
968 | Animations
969 | ******* */
970 |
971 | .fade-enter {
972 | opacity: 0.01;
973 | }
974 |
975 | .fade-enter.fade-enter-active {
976 | opacity: 1;
977 | transition: opacity 500ms ease-in;
978 | }
979 |
980 | .fade-leave {
981 | opacity: 1;
982 | }
983 |
984 | .fade-leave.fade-leave-active {
985 | opacity: 0.01;
986 | transition: opacity 300ms ease-in;
987 | }
988 |
989 | .fade-appear {
990 | opacity: 0.01;
991 | }
992 |
993 | .fade-appear.fade-appear-active {
994 | opacity: 1;
995 | transition: opacity .5s ease-in;
996 | }
997 |
998 | @keyframes react-spinner_spin {
999 | 0% { opacity: 1; }
1000 | 100% { opacity: 0.15; }
1001 | }
1002 |
1003 | @-moz-keyframes react-spinner_spin {
1004 | 0% { opacity: 1; }
1005 | 100% { opacity: 0.15; }
1006 | }
1007 |
1008 | @-webkit-keyframes react-spinner_spin {
1009 | 0% { opacity: 1; }
1010 | 100% { opacity: 0.15; }
1011 | }
1012 |
1013 |
1014 | @media (min-width: 768px) {
1015 | .container {
1016 | width: 732px;
1017 | }
1018 | }
1019 |
1020 | @media (min-width: 992px) {
1021 | .container {
1022 | width: 952px;
1023 | }
1024 | }
1025 |
1026 | @media (min-width: 1200px) {
1027 | .container {
1028 | width: 1152px;
1029 | }
1030 | }
--------------------------------------------------------------------------------
/dev/fonts/IcoMoon-Free.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grobjin9/react.js-voice-audio-player/d57f06b0d7cfd3497e0b66c65775fa6cce4eb8f4/dev/fonts/IcoMoon-Free.ttf
--------------------------------------------------------------------------------
/dev/fonts/Read Me.txt:
--------------------------------------------------------------------------------
1 | In this folder, you can find the IcoMoon-Free font in TTF format. You can install this font so that you can use it in desktop applications.
2 |
3 | Open "Reference.html" to see a list of the icons available in this font. The text box located to the bottom right of each icon contains a character (which may be invisible). You can copy and use this character in any desktop application that allows entering text and choosing a custom font to display it. You may also type or copy the text in the "liga" field if the environment in which you're using the font supports ligatures.
4 |
5 | To get crisp results, use font sizes that are a multiple of 16px.
6 |
7 | It is not recommend to use this font on the web. To make an optimized webfont, use the IcoMoon app (https://icomoon.io/app). This app allows you to choose the icons that you need and make them into webfonts.
8 |
9 | You can import "selection.json" to the IcoMoon app to modify this font.
10 |
--------------------------------------------------------------------------------
/dev/js/actions/dataActions.js:
--------------------------------------------------------------------------------
1 | const constants = require('../constants/dataConstants');
2 |
3 | const updateData = function (data) {
4 | return {
5 | type: constants.UPDATE_DATA,
6 | data
7 | };
8 | };
9 |
10 | module.exports = {
11 | updateData
12 | };
--------------------------------------------------------------------------------
/dev/js/actions/favoritesActions.js:
--------------------------------------------------------------------------------
1 | const constants = require('../constants/favoritesConstants');
2 |
3 | const fetchFavoritesStart = function () {
4 | return {
5 | type: constants.FETCH_FAVORITES_START
6 | };
7 | };
8 |
9 | const removeTrack = function (id) {
10 | return {
11 | type: constants.REMOVE_TRACK,
12 | id
13 | };
14 | };
15 |
16 | const updateFavorites = function (tracks) {
17 | return {
18 | type: constants.UPDATE_FAVORITES,
19 | tracks
20 | };
21 | };
22 |
23 | const fetchFavoritesError = function (error) {
24 | return {
25 | type: constants.FETCH_FAVORITES_ERROR,
26 | error
27 | };
28 | };
29 |
30 | const updateAmount = function (amount) {
31 | return {
32 | type: constants.UPDATE_AMOUNT,
33 | amount
34 | };
35 | };
36 |
37 | const shuffleTracks = function () {
38 | return {
39 | type: constants.SHUFFLE_TRACKS
40 | };
41 | };
42 |
43 | module.exports = {
44 | fetchFavoritesStart,
45 | updateFavorites,
46 | fetchFavoritesError,
47 | updateAmount,
48 | removeTrack,
49 | shuffleTracks
50 | };
--------------------------------------------------------------------------------
/dev/js/actions/playerActions.js:
--------------------------------------------------------------------------------
1 | const constants = require('../constants/playerConstants');
2 |
3 | const changePlayingTrack = function (trackIndex) {
4 | return {
5 | type: constants.CHANGE_PLAYING_TRACK,
6 | trackIndex
7 | };
8 | };
9 |
10 | const changeCurrentTime = function (time) {
11 | return {
12 | type: constants.CHANGE_CURRENT_TIME,
13 | time
14 | };
15 | };
16 |
17 | const toggleTrack = function (isPlaying) {
18 | return {
19 | type: constants.TOGGLE_TRACK,
20 | isPlaying
21 | };
22 | };
23 |
24 | const toggleRepeatTrack = function () {
25 | return {
26 | type: constants.TOGGLE_REPEAT_TRACK
27 | };
28 | };
29 |
30 | const changeTrack = function (eType) {
31 | return function (dispatch, getState) {
32 | const {playlist, player} = getState();
33 |
34 | let index = player.currentTrackIndex,
35 | incIndex = index + 1,
36 | decIndex = index - 1,
37 | nextTrackIndex;
38 |
39 | if (eType === constants.NEXT_TRACK) {
40 | if (incIndex > playlist.tracks.length - 1) {
41 | incIndex = playlist.tracks.length - 1;
42 | }
43 |
44 | nextTrackIndex = incIndex;
45 | } else if (eType === constants.PREV_TRACK) {
46 | if (decIndex < 0) {
47 | decIndex = 0;
48 | }
49 |
50 | nextTrackIndex = decIndex;
51 | }
52 |
53 | dispatch(changePlayingTrack(nextTrackIndex));
54 | dispatch(changeCurrentTime(0));
55 | };
56 | };
57 |
58 | const playTrack = function (trackIndex) {
59 | return function (dispatch, getState) {
60 | const {player} = getState();
61 |
62 | if (trackIndex === player.currentTrackIndex) {
63 | console.log('this');
64 | return;
65 | }
66 |
67 | dispatch(changeCurrentTime(0));
68 | dispatch(changePlayingTrack(trackIndex));
69 |
70 | };
71 | };
72 |
73 | module.exports = {
74 | changePlayingTrack,
75 | changeCurrentTime,
76 | changeTrack,
77 | playTrack,
78 | toggleTrack,
79 | toggleRepeatTrack
80 | };
--------------------------------------------------------------------------------
/dev/js/actions/playlistActions.js:
--------------------------------------------------------------------------------
1 | const constants = require('../constants/playlistConstants');
2 |
3 | const fetchPlaylistStart = function () {
4 | return {
5 | type: constants.FETCH_PLAYLIST_START
6 | };
7 | };
8 |
9 | const updatePlaylist = function (tracks) {
10 | return {
11 | type: constants.UPDATE_PLAYLIST,
12 | tracks
13 | };
14 | };
15 |
16 | const fetchPlaylistError = function (error) {
17 | return {
18 | type: constants.FETCH_PLAYLIST_ERROR,
19 | error
20 | };
21 | };
22 |
23 | const updateFavoriteTracks = function (tracks) {
24 | return {
25 | type: constants.UPDATE_FAVORITE_TRACKS,
26 | tracks
27 | };
28 | };
29 |
30 | const concatPartialTracks = function (tracks) {
31 | return {
32 | type: constants.CONCAT_PARTIAL_TRACKS,
33 | tracks
34 | };
35 | };
36 |
37 | const changeSearchText = function (text) {
38 | return {
39 | type: constants.UPDATE_SEARCH_TEXT,
40 | text
41 | };
42 | };
43 |
44 | const shuffleTracks = function () {
45 | return {
46 | type: constants.SHUFFLE_TRACKS
47 | };
48 | };
49 |
50 | module.exports = {
51 | fetchPlaylistStart,
52 | updatePlaylist,
53 | fetchPlaylistError,
54 | updateFavoriteTracks,
55 | changeSearchText,
56 | concatPartialTracks,
57 | shuffleTracks
58 | };
--------------------------------------------------------------------------------
/dev/js/actions/uiActions.js:
--------------------------------------------------------------------------------
1 | const constants = require('../constants/uiConstants');
2 |
3 | const changeCurrentTab = function (tab) {
4 | return {
5 | type: constants.CHANGE_CURRENT_TAB,
6 | tab
7 | };
8 | };
9 |
10 | module.exports = {
11 | changeCurrentTab
12 | };
--------------------------------------------------------------------------------
/dev/js/components/Content.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Playlist = require('../containers/Playlist'),
3 | Artwork = require('./../containers/Artwork');
4 |
5 | class Content extends React.Component {
6 |
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
Your browser needs to be updated ASAP
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | module.exports = Content;
--------------------------------------------------------------------------------
/dev/js/components/ControlBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | ActionBar = require('../containers/ActionBar'),
3 | TimeBar = require('../containers/TimeBar'),
4 | ProcessingBar = require('../containers/ProcessingBar');
5 |
6 |
7 | class ControlBar extends React.Component {
8 | render() {
9 | return (
10 |
17 | );
18 | }
19 | }
20 |
21 | module.exports = ControlBar;
--------------------------------------------------------------------------------
/dev/js/components/ControlBarButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class Button extends React.Component {
4 | shouldComponentUpdate(nextProps, nextState) {
5 | return this.props.buttonType !== nextProps.buttonType;
6 | }
7 |
8 | render() {
9 | return (
10 |
11 | );
12 | }
13 |
14 | static propTypes = {
15 | buttonType: React.PropTypes.string.isRequired,
16 | id: React.PropTypes.string.isRequired,
17 | btnOnClick: React.PropTypes.func.isRequired
18 | }
19 | }
20 |
21 | module.exports = Button;
--------------------------------------------------------------------------------
/dev/js/components/Cover.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | ReactDOM = require('react-dom'),
3 | Spinner = require('react-spinner');
4 |
5 | class Cover extends React.Component {
6 |
7 | render() {
8 | let {src, coverOnLoad, loading} = this.props;
9 |
10 | let spinnerStyle = {
11 | width: 30,
12 | height: 30,
13 | display: loading ? 'block' : 'none'
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | static propTypes = {
25 | src: React.PropTypes.string.isRequired,
26 | coverOnLoad: React.PropTypes.func.isRequired,
27 | loading: React.PropTypes.bool
28 | }
29 | }
30 |
31 | module.exports = Cover;
--------------------------------------------------------------------------------
/dev/js/components/CurrentTabPicker.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class CurrentTabPicker extends React.Component {
4 | render() {
5 | let {tabOnChanged, currentTab, playlistTracksLength, favoriteTracksLength} = this.props;
6 |
7 | return (
8 |
9 |
15 | Playlist
16 | {playlistTracksLength}
17 |
18 |
19 |
26 | Favorites
27 | {favoriteTracksLength}
28 |
29 | );
30 | }
31 |
32 | static propTypes = {
33 | tabOnChanged: React.PropTypes.func.isRequired,
34 | currentTab: React.PropTypes.string.isRequired,
35 | playlistTracksLength: React.PropTypes.number.isRequired,
36 | favoriteTracksLength: React.PropTypes.number.isRequired
37 | }
38 | }
39 |
40 | module.exports = CurrentTabPicker;
--------------------------------------------------------------------------------
/dev/js/components/CurrentTimeBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | formatMStoS = require('../utils/format').formatMStoS;
3 |
4 | const CurrentTimeBar = function (props) {
5 | return (
6 |
7 | {formatMStoS(props.currentTime)}
8 |
9 | );
10 | };
11 |
12 | CurrentTimeBar.propTypes = {
13 | currentTime: React.PropTypes.number.isRequired
14 | };
15 |
16 | module.exports = CurrentTimeBar;
--------------------------------------------------------------------------------
/dev/js/components/DurationBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | formatMStoS = require('../utils/format').formatMStoS;
3 |
4 | class DurationBar extends React.Component {
5 | shouldComponentUpdate(nextProps, nextState) {
6 | return nextProps.duration !== this.props.duration;
7 | }
8 |
9 | render() {
10 | return (
11 |
12 | {formatMStoS(this.props.duration)}
13 |
14 | );
15 | }
16 |
17 | static propTypes = {
18 | duration: React.PropTypes.number.isRequired
19 | }
20 | }
21 |
22 | module.exports = DurationBar;
--------------------------------------------------------------------------------
/dev/js/components/Equalizer.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Tooltip = require('./Tooltip'),
3 | EqualizerVerticalSlider = require('./sliders/EqualizerVerticalSlider');
4 |
5 | class Equalizer extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | active: false
11 | };
12 |
13 | this.equalizerOnClick = this.equalizerOnClick.bind(this);
14 | }
15 |
16 | equalizerOnClick(e) {
17 | if (e.target !== this.equalizer) return;
18 |
19 | this.setState({
20 | active: !this.state.active
21 | });
22 |
23 | document.documentElement.click();
24 | document.onclick = null;
25 |
26 | if (!this.state.active) {
27 | document.onclick = (e) => {
28 | if (e.target.closest('#equalizer') !== this.equalizer) {
29 |
30 | this.setState({
31 | active: !this.state.active
32 | });
33 |
34 | document.onclick = null;
35 | }
36 | };
37 | }
38 | }
39 |
40 | render() {
41 | let types = ['frequency', 'gain'],
42 | sliders = [{title: 'BASS', types}, {title: 'MID', types}, {title: 'TREBLE', types}],
43 | handler = this.props.filterFrequencies;
44 |
45 | return (
46 | this.equalizer = eq }>
50 |
51 | {sliders.map(slider => (
52 |
53 | ))}
54 |
55 |
56 | );
57 | }
58 |
59 | static propTypes = {
60 | filterFrequencies: React.PropTypes.func.isRequired
61 | }
62 | }
63 |
64 | module.exports = Equalizer;
--------------------------------------------------------------------------------
/dev/js/components/FavoritesItem.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | formatTitle = require('../utils/format').formatTitle;
3 |
4 | class FavoritesItem extends React.Component {
5 |
6 | shouldComponentUpdate(nextProps) {
7 | return !!(nextProps.active !== this.props.active || nextProps.isFavorite !== this.props.isFavorite); // (:
8 | }
9 |
10 | render() {
11 | let {track, trackOnClick, active, starOnClick} = this.props;
12 |
13 | return (
14 | trackOnClick(e, track)}
15 | className={"clearfix playlist__item " + (active ? 'currentTrack' : '')}>
16 | {track.index + 1}
17 | {formatTitle(track.title)}
18 | starOnClick(e, track)}
20 | data-val="star"
21 | className={'icon track-star track-star__favorite'}>
22 |
23 |
24 | );
25 | }
26 |
27 | static propTypes = {
28 | track: React.PropTypes.object.isRequired,
29 | trackOnClick: React.PropTypes.func.isRequired,
30 | starOnClickReact: React.PropTypes.func.isRequired,
31 | active: React.PropTypes.bool.isRequired
32 | }
33 | }
34 |
35 | module.exports = FavoritesItem;
--------------------------------------------------------------------------------
/dev/js/components/Header.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | SearchBar = require('./../containers/SearchBar'),
3 | TrackData = require('./../containers/TrackData'),
4 | VoiceBar = require('./../containers/VoiceBar');
5 |
6 | class Header extends React.Component {
7 | render() {
8 | return (
9 |
20 | );
21 | }
22 | }
23 |
24 | module.exports = Header;
--------------------------------------------------------------------------------
/dev/js/components/Playlist.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Tab = require('./Tab'),
3 | Spinner = require('react-spinner'),
4 | CurrentTabPicker = require('./CurrentTabPicker');
5 |
6 | class Playlist extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.tabOnChanged = this.tabOnChanged.bind(this);
11 | }
12 |
13 | tabOnChanged(e) {
14 | const tabOnChanged = this.props.tabOnChanged;
15 |
16 | tabOnChanged(e.target.value);
17 | }
18 |
19 | render() {
20 | const {playlist, data, currentTrackIndex, currentTab, favorites, starOnClick, trackOnClick, trackListOnScrolled, fetching} = this.props,
21 | {tracks, searchText:playlistSearchText} = playlist,
22 | {currentPlaylist:playingTab, id: currentTrackId, searchText:trackSearchText} = data,
23 | {amount:favoritesAmount, tracks:favoriteTracks} = favorites;
24 |
25 | return (
26 |
27 |
33 |
34 |
49 |
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | static propTypes = {
67 | playlist: React.PropTypes.object.isRequired,
68 | data: React.PropTypes.object.isRequired,
69 | currentTrackIndex: React.PropTypes.number,
70 | currentTab: React.PropTypes.string.isRequired,
71 | favorites: React.PropTypes.object.isRequired,
72 | fetching: React.PropTypes.bool,
73 | starOnClick: React.PropTypes.func.isRequired,
74 | trackOnClick: React.PropTypes.func.isRequired,
75 | trackListOnScrolled: React.PropTypes.func.isRequired
76 | }
77 | }
78 |
79 | module.exports = Playlist;
--------------------------------------------------------------------------------
/dev/js/components/PlaylistItem.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class PlaylistItem extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 |
8 | this.titleOnMouseOver = this.titleOnMouseOver.bind(this);
9 | this.titleOnMouseOut = this.titleOnMouseOut.bind(this);
10 | }
11 |
12 | shouldComponentUpdate(nextProps) {
13 | return !!(nextProps.active !== this.props.active || nextProps.isFavorite !== this.props.isFavorite);
14 | }
15 |
16 | titleOnMouseOver() {
17 | if (this.title.offsetWidth < this.title.scrollWidth) {
18 | this.title.style.textIndent = -(this.title.scrollWidth - this.title.offsetWidth) + 'px';
19 | }
20 | }
21 |
22 | titleOnMouseOut() {
23 | this.title.style.textIndent = '0px';
24 | }
25 |
26 | render() {
27 | let {track, trackOnClick, active, isFavorite, starOnClick} = this.props;
28 |
29 | return (
30 | trackOnClick(e, track)}
31 | className={"clearfix playlist__item " + (active ? 'currentTrack' : '')}>
32 | {track.index + 1}
33 | this.title = t}
36 | className="track-title">{track.title}
37 |
38 | starOnClick(e, track)}
40 | data-val="star"
41 | className={'icon track-star ' + (isFavorite ? 'track-star__favorite' : '')}>
42 |
43 |
44 | );
45 | }
46 |
47 | static propTypes = {
48 | track: React.PropTypes.object.isRequired,
49 | trackOnClick: React.PropTypes.func.isRequired,
50 | starOnClick: React.PropTypes.func.isRequired,
51 | isFavorite: React.PropTypes.bool.isRequired,
52 | active: React.PropTypes.bool.isRequired
53 | }
54 | }
55 |
56 | module.exports = PlaylistItem;
--------------------------------------------------------------------------------
/dev/js/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | getCoords = require('../utils/dom').getCoords,
3 | playerAPI = require('../utils/playerAPI');
4 |
5 | class TrackProgressBar extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | tooltipVisibility: false
12 | };
13 |
14 | this.progressBarOnDrag = this.progressBarOnDrag.bind(this);
15 | this.sliderOnClick = this.sliderOnClick.bind(this);
16 | this._documentOnMouseMove = this._documentOnMouseMove.bind(this);
17 | }
18 |
19 | componentDidMount() {
20 | this.sliderWidth = this.slider.offsetWidth;
21 | this.thumbWidth = this.thumb.offsetWidth;
22 |
23 | this.endPoint = this.sliderWidth - this.thumbWidth;
24 |
25 | this._cfg = {
26 | MIN: 0,
27 | MAX: this.slider.offsetWidth - this.thumb.offsetWidth
28 | };
29 | }
30 |
31 | _documentOnMouseMove(e, shiftX = 0) {
32 | playerAPI.pause();
33 |
34 | let updateTime = this.props.updateTime,
35 | move = e.pageX - getCoords(this.slider).left - (shiftX || this.thumbWidth / 2),
36 | ratio = move / this._cfg.MAX;
37 |
38 | if (ratio <= 0) {
39 | ratio = 0;
40 | } else if (ratio >= 1) {
41 | ratio = 1;
42 | }
43 |
44 | this.thumb.style.left = ratio * this._cfg.MAX + 'px';
45 | this.counter.style.width = ratio * this._cfg.MAX + (this.thumb ? this.thumbWidth / 2 : 0.3) + 'px';
46 |
47 | document.onmouseup = function () {
48 | updateTime(ratio);
49 |
50 | this.onmousemove = this.onmouseup = null;
51 | };
52 |
53 | }
54 |
55 | progressBarOnDrag(e) {
56 | let shiftX = e.pageX - getCoords(this.thumb).left,
57 | updateTime = this.props.updateTime;
58 |
59 | playerAPI.pause();
60 |
61 | document.onmousemove = (e) => {
62 | let move = e.pageX - getCoords(this.slider).left - shiftX,
63 | ratio = move / this._cfg.MAX;
64 |
65 | if (ratio <= 0) {
66 | ratio = 0;
67 | } else if (ratio >= 1) {
68 | ratio = 1;
69 | }
70 |
71 | this.thumb.style.left = ratio * this._cfg.MAX + 'px';
72 | this.counter.style.width = ratio * this._cfg.MAX + this.thumbWidth / 2 + 'px';
73 |
74 | document.onmouseup = function () {
75 | updateTime(ratio);
76 |
77 | this.onmousemove = this.onmouseup = null;
78 | };
79 | };
80 | }
81 |
82 | sliderOnClick(e) {
83 | if (e.target === this.thumb) return;
84 |
85 | let updateTime = this.props.updateTime,
86 | move = e.pageX - getCoords(this.slider).left - this.thumbWidth / 2,
87 | ratio = move / this._cfg.MAX;
88 |
89 | if (ratio <= 0) {
90 | ratio = 0;
91 | } else if (ratio >= 1) {
92 | ratio = 1;
93 | }
94 |
95 | updateTime(ratio);
96 | }
97 |
98 | render() {
99 | let progress = this.props.progress,
100 | thumbStyle = {left: progress * this.endPoint + 'px'},
101 | counterStyle = {width: progress * this.endPoint + this.thumbWidth / 2 + 'px'};
102 |
103 | return (
104 |
105 | this.slider = s}>
108 |
this.thumb = th}>
113 |
114 |
this.counter = c}>
115 |
116 |
117 | );
118 | }
119 |
120 | static propTypes = {
121 | updateTime: React.PropTypes.func.isRequired,
122 | progress: React.PropTypes.number.isRequired
123 | }
124 | }
125 |
126 | module.exports = TrackProgressBar;
--------------------------------------------------------------------------------
/dev/js/components/Tab.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | getCoords = require('../utils/dom').getCoords,
3 | FavoritesItem = require('./FavoritesItem'),
4 | PlaylistItem = require('./PlaylistItem'),
5 | TracksList = require('./TracksList');
6 |
7 | let lastTimeValue = 0,
8 | timer = null;
9 |
10 | class Tab extends React.Component {
11 |
12 | constructor(props) {
13 | super(props);
14 |
15 | this.sbOnMouseDown = this.sbOnMouseDown.bind(this);
16 | this.listOnWheel = this.listOnWheel.bind(this);
17 | this.updateScrollBarPos = this.updateScrollBarPos.bind(this);
18 | }
19 |
20 | componentDidMount() {
21 | this.scrollBarParent = this.scrollBar.parentElement;
22 |
23 | const self = this;
24 |
25 | this._cfg = {
26 | MIN: 0,
27 | MAX: self.scrollBarParent.offsetHeight - self.scrollBar.offsetHeight
28 | };
29 | }
30 |
31 | componentWillUpdate(nextProps) {
32 | if (nextProps.playlistSearchText !== this.props.playlistSearchText) {
33 | this.list.scrollTop = 0;
34 | this.scrollBar.style.top = '0px';
35 | }
36 | }
37 |
38 |
39 | sbOnMouseDown(e) {
40 | const shiftY = e.pageY - getCoords(this.scrollBar).top;
41 |
42 | document.onmousemove = (e) => {
43 | let move = e.pageY - shiftY - getCoords(this.scrollBarParent).top,
44 | ratio = move / (this.scrollBarParent.offsetHeight - this.scrollBar.offsetHeight),
45 | newTimeValue = new Date(),
46 | delay = 550;
47 |
48 | if (ratio <= 0) {
49 | ratio = 0;
50 | } else if (ratio >= 1) {
51 | ratio = 1;
52 | }
53 |
54 | this.updateScrollBarPos(ratio);
55 |
56 | this.list.scrollTop = ratio * (this.list.scrollHeight - this.list.offsetHeight);
57 |
58 | if (ratio >= 1 && this.props.currentTab === 'playlist') {
59 | if ((newTimeValue - lastTimeValue) < delay) {
60 | clearTimeout(timer);
61 | }
62 |
63 | timer = setTimeout(() => {
64 | let evt = {
65 | deltaY: 16
66 | };
67 |
68 | this.props.trackListOnScrolled(evt, this.listOnWheel);
69 | }, delay);
70 |
71 | lastTimeValue = newTimeValue;
72 | }
73 | };
74 |
75 | document.onmouseup = () => {
76 | document.onmousemove = document.onmouseup = null;
77 | };
78 | }
79 |
80 | updateScrollBarPos(ratio) {
81 | this.scrollBar.style.top = ratio * (this.list.offsetHeight - this.scrollBar.offsetHeight) + 'px';
82 | }
83 |
84 | listOnWheel(e) {
85 | let newTimeValue = new Date(),
86 | delay = 550;
87 |
88 | this.list.scrollTop += e.deltaY / 2;
89 |
90 | let scrollRatio = (this.list.scrollTop / ( this.list.scrollHeight - this.list.offsetHeight ));
91 |
92 | this.updateScrollBarPos(scrollRatio);
93 |
94 | if (scrollRatio >= 1 && this.props.currentTab === 'playlist') {
95 | if ((newTimeValue - lastTimeValue) < delay) {
96 | clearTimeout(timer);
97 | }
98 |
99 | timer = setTimeout(() => {
100 | this.props.trackListOnScrolled(e, this.listOnWheel);
101 | }, delay);
102 |
103 | lastTimeValue = newTimeValue;
104 | }
105 | }
106 |
107 | render() {
108 | let {currentTab, name, tracks} = this.props,
109 | active = tracks.length > 6,
110 | scrollBarStyle = {visibility: active ? 'visible' : 'hidden'};
111 |
112 | return (
113 |
114 |
this.scrollBar = sb}
115 | onMouseDown={this.sbOnMouseDown}
116 | className="list-scroller"
117 | style={scrollBarStyle}>
118 |
119 |
120 |
this.list = l}>
121 |
122 |
123 |
124 | );
125 | }
126 |
127 | static propTypes = {
128 | currentTab: React.PropTypes.string.isRequired,
129 | name: React.PropTypes.string.isRequired,
130 | tracks: React.PropTypes.array.isRequired,
131 | playlistSearchText: React.PropTypes.string
132 | }
133 | }
134 |
135 | module.exports = Tab;
--------------------------------------------------------------------------------
/dev/js/components/Tooltip.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class Tooltip extends React.Component {
4 | render() {
5 | let {visible, title, size, children} = this.props,
6 | sizeClass;
7 |
8 | if (size === 'm') {
9 | sizeClass = '--medium';
10 | } else if (size === 's') {
11 | sizeClass = '--small';
12 | } else {
13 | sizeClass = '--medium';
14 | }
15 |
16 | let titleElem = (
17 |
18 | {title}
19 |
20 | );
21 |
22 | return (
23 |
25 |
1 ? children.length * 30 : ''}}>
26 | {(this.props.children ? this.props.children : '')}
27 |
28 | {title ? titleElem : ''}
29 |
30 | )
31 | }
32 |
33 | static propTypes = {
34 | visible: React.PropTypes.bool.isRequired,
35 | title: React.PropTypes.string,
36 | size: React.PropTypes.string,
37 | children: React.PropTypes.node.isRequired
38 | }
39 | }
40 |
41 | module.exports = Tooltip;
42 |
--------------------------------------------------------------------------------
/dev/js/components/TracksList.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | localStore = require('../utils/localStore'),
3 | ReactCSSTransitionGroup = require('react-addons-css-transition-group'),
4 | PlaylistItem = require('./PlaylistItem');
5 |
6 | class TracksList extends React.Component {
7 |
8 | render() {
9 | let {
10 | currentTab, playingTab, activeIndex, tracks, currentTrackId,
11 | trackOnClick, starOnClick, trackSearchText, playlistSearchText
12 | } = this.props,
13 | Item = PlaylistItem;
14 |
15 | if (tracks.length) {
16 | tracks = tracks.map(function (track) {
17 | let isFavorite = localStore.includes(track.id),
18 | isActive = (track.index === activeIndex) &&
19 | (currentTab === playingTab) &&
20 | (trackSearchText === playlistSearchText) &&
21 | (currentTrackId === tracks[activeIndex].id);
22 |
23 | return (
24 |
31 |
38 |
39 | );
40 | });
41 | }
42 |
43 | return (
44 |
45 | {tracks}
46 |
47 | );
48 | }
49 |
50 | static propTypes = {
51 | currentTab: React.PropTypes.string.isRequired,
52 | playingTab: React.PropTypes.string,
53 | activeIndex: React.PropTypes.number,
54 | tracks: React.PropTypes.array.isRequired,
55 | currentTrackId: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]).isRequired,
56 | trackOnClick: React.PropTypes.func.isRequired,
57 | starOnClick: React.PropTypes.func.isRequired,
58 | trackSearchText: React.PropTypes.string,
59 | playlistSearchText: React.PropTypes.string
60 | }
61 | }
62 |
63 | module.exports = TracksList;
--------------------------------------------------------------------------------
/dev/js/components/VolumeBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Tooltip = require('./Tooltip'),
3 | VerticalSlider = require('./sliders/VerticalSlider');
4 |
5 | class VolumeBar extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | active: false,
11 | volume: 'high'
12 | };
13 |
14 | this.volumeBarOnClick = this.volumeBarOnClick.bind(this);
15 | this.sliderOnClick = this.sliderOnClick.bind(this);
16 | this.thumbOnMouseMove = this.thumbOnMouseMove.bind(this);
17 | }
18 |
19 | volumeBarOnClick(e) {
20 | if (e.target !== this.volumeBar) return;
21 |
22 | this.setState({
23 | active: !this.state.active
24 | });
25 |
26 | document.documentElement.click();
27 | document.onclick = null;
28 |
29 | if (!this.state.active) {
30 | document.onclick = (e) => {
31 | if (e.target.closest('#volume') !== this.volumeBar) {
32 |
33 | this.setState({
34 | active: !this.state.active
35 | });
36 |
37 | document.onclick = null;
38 | }
39 | };
40 | }
41 | }
42 |
43 | sliderOnClick(volume) {
44 | let setVolume = this.props.setVolume;
45 |
46 | this.setState({
47 | volume: VolumeBar.getCurrentVolStatus(volume)
48 | });
49 |
50 | setVolume(volume);
51 | }
52 |
53 | thumbOnMouseMove(volume) {
54 | let setVolume = this.props.setVolume;
55 |
56 | this.setState({
57 | volume: VolumeBar.getCurrentVolStatus(volume)
58 | });
59 |
60 | setVolume(volume);
61 | }
62 |
63 | render() {
64 | let volumeStatus = this.state.volume;
65 |
66 | return (
67 | this.volumeBar = vb }>
71 |
72 |
78 |
79 |
80 | );
81 | }
82 |
83 | static getCurrentVolStatus(vol) {
84 | if (vol === 0) {
85 | return 'mute';
86 | } else if (vol <= 0.4) {
87 | return 'low'
88 | } else if (vol <= 0.7) {
89 | return 'medium'
90 | } else {
91 | return 'high'
92 | }
93 | }
94 |
95 | static propTypes = {
96 | setVolume: React.PropTypes.func.isRequired
97 | }
98 | }
99 |
100 | module.exports = VolumeBar;
--------------------------------------------------------------------------------
/dev/js/components/buttons/NextButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Button = require('../ControlBarButton'),
3 | playerAPI = require('../../utils/playerAPI');
4 |
5 | class NextButton extends React.Component {
6 |
7 | shouldComponentUpdate() {
8 | return false;
9 | }
10 |
11 | render() {
12 | let id = "action-bar__next",
13 | buttonType = "next";
14 |
15 | return (
16 |
17 | );
18 | }
19 |
20 | static propTypes = {
21 | nextBtnOnClick: React.PropTypes.func.isRequired
22 | }
23 | }
24 |
25 | module.exports = NextButton;
--------------------------------------------------------------------------------
/dev/js/components/buttons/PlayButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Button = require('../ControlBarButton');
3 |
4 | const PlayButton = function (props) {
5 | let id = "action-bar__play",
6 | buttonType = (props.isPlaying ? 'stop' : 'play');
7 |
8 | return (
9 |
10 | );
11 | };
12 |
13 | PlayButton.propTypes = {
14 | playBtnOnClick: React.PropTypes.func.isRequired,
15 | isPlaying: React.PropTypes.bool.isRequired
16 | };
17 |
18 | module.exports = PlayButton;
--------------------------------------------------------------------------------
/dev/js/components/buttons/PrevButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Button = require('../ControlBarButton'),
3 | playerAPI = require('../../utils/playerAPI');
4 |
5 | class PrevButton extends React.Component {
6 |
7 | shouldComponentUpdate() {
8 | return false;
9 | }
10 |
11 | render() {
12 | let id = "action-bar__prev",
13 | buttonType = "prev";
14 |
15 | return (
16 |
17 | );
18 | }
19 |
20 | static propTypes = {
21 | prevBtnOnClick: React.PropTypes.func.isRequired
22 | }
23 | }
24 |
25 | module.exports = PrevButton;
26 |
--------------------------------------------------------------------------------
/dev/js/components/buttons/RepeatButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Button = require('../ControlBarButton'),
3 | playerAPI = require('../../utils/playerAPI');
4 |
5 | class ShuffleButton extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.btnOnClick = this.btnOnClick.bind(this);
11 | }
12 |
13 | btnOnClick() {
14 | console.log('...');
15 | }
16 |
17 | render() {
18 | let id = "processing-bar__shuffle",
19 | buttonType = "shuffle";
20 |
21 | return (
22 |
23 | );
24 | }
25 | }
26 |
27 | module.exports = ShuffleButton;
--------------------------------------------------------------------------------
/dev/js/components/buttons/ShuffleButton.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Button = require('../ControlBarButton'),
3 | playerAPI = require('../../utils/playerAPI');
4 |
5 | class ShuffleButton extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.btnOnClick = this.btnOnClick.bind(this);
11 | }
12 |
13 | btnOnClick() {
14 | console.log('...');
15 | }
16 |
17 | render() {
18 | let id = "processing-bar__shuffle",
19 | buttonType = "shuffle";
20 |
21 | return (
22 |
23 | );
24 | }
25 | }
26 |
27 | module.exports = ShuffleButton;
--------------------------------------------------------------------------------
/dev/js/components/buttons/SpeakerButton.js:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/js/components/sliders/EqualizerVerticalSlider.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | VerticalSliderItem = require('./VerticalSliderItem');
3 |
4 | class EqualizerVerticalSlider extends React.Component {
5 | render() {
6 | let {handler, data, len} = this.props,
7 | items = data.types.map(item => (
8 |
9 | ));
10 |
11 | return (
12 |
13 | {items}
14 |
15 | { data.title }
16 |
17 |
18 | );
19 | }
20 |
21 | static propTypes = {
22 | handler: React.PropTypes.func.isRequired,
23 | data: React.PropTypes.object.isRequired,
24 | len: React.PropTypes.number.isRequired
25 | }
26 | }
27 |
28 | module.exports = EqualizerVerticalSlider;
--------------------------------------------------------------------------------
/dev/js/components/sliders/VerticalSlider.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | getCoords = require('../../utils/dom').getCoords,
3 | VerticalSliderItem = require('./VerticalSliderItem');
4 |
5 | class VerticalSlider extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.sliderOnOwnClick = this.sliderOnOwnClick.bind(this);
11 | this.thumbOnOwnMouseMove = this.thumbOnOwnMouseMove.bind(this);
12 | }
13 |
14 | componentDidMount() {
15 | this.sliderHeight = this.slider.offsetHeight;
16 | this.thumbHeight = this.thumb.offsetHeight;
17 | this._cfg = {
18 | MIN: 0,
19 | MAX: this.slider.offsetHeight - this.thumb.offsetHeight
20 | };
21 |
22 | let thumbTop = (this.props.initVal - 1) * this._cfg.MAX;
23 |
24 | this.thumb.style.top = thumbTop + 'px';
25 | this.counter.style.height = this._cfg.MAX - thumbTop + this.thumbHeight / 2 + 'px';
26 | }
27 |
28 | thumbOnOwnMouseMove(e) {
29 | let thumbOnMouseMove = this.props.thumbOnMouseMove,
30 | shiftY = e.pageY - getCoords(this.thumb).top;
31 |
32 | document.onmousemove = (e) => {
33 | let move = e.pageY - getCoords(this.slider).top - shiftY,
34 | ratio = 1 - move / this._cfg.MAX;
35 |
36 | if (ratio <= 0) {
37 | ratio = 0;
38 | move = this._cfg.MAX;
39 | } else if (ratio >= 1) {
40 | ratio = 1;
41 | move = 0;
42 | }
43 |
44 | this.thumb.style.top = move + 'px';
45 | this.counter.style.height = this._cfg.MAX - move + this.thumbHeight / 2 + 'px';
46 |
47 | thumbOnMouseMove(ratio);
48 |
49 | document.onmouseup = (e) => {
50 | document.onmousemove = document.onmouseup = null;
51 | };
52 |
53 | };
54 | }
55 |
56 | sliderOnOwnClick(e) {
57 | if (e.target == this.thumb) return;
58 |
59 | let sliderOnClick = this.props.sliderOnClick,
60 | move = e.pageY - getCoords(this.slider).top - this.thumbHeight / 2,
61 | ratio = 1 - move / this._cfg.MAX;
62 |
63 | if (ratio <= 0) {
64 | ratio = 0;
65 | move = this._cfg.MAX;
66 | } else if (ratio >= 1) {
67 | ratio = 1;
68 | move = 0;
69 | }
70 |
71 | this.thumb.style.top = move + 'px';
72 | this.counter.style.height = this._cfg.MAX - move + this.thumbHeight / 2 + 'px';
73 |
74 | sliderOnClick(ratio);
75 | }
76 |
77 | render() {
78 | let {title} = this.props,
79 | titleElem;
80 |
81 | if (title) {
82 | titleElem = (
83 |
84 | { title }
85 |
86 | )
87 | }
88 |
89 | return (
90 |
91 |
92 |
this.slider = s}>
95 |
this.thumb = th}>
99 |
100 |
this.counter = c}>
102 |
103 |
104 |
105 | { titleElem }
106 |
107 | );
108 | }
109 |
110 | static propTypes = {
111 | title: React.PropTypes.string,
112 | sliderOnClick: React.PropTypes.func.isRequired,
113 | thumbOnMouseMove: React.PropTypes.func.isRequired,
114 | initVal: React.PropTypes.number.isRequired
115 |
116 | }
117 | }
118 |
119 | module.exports = VerticalSlider;
--------------------------------------------------------------------------------
/dev/js/components/sliders/VerticalSliderItem.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | getCoords = require('../../utils/dom').getCoords;
3 |
4 | class VerticalSliderItem extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 |
9 | this.sliderOnOwnClick = this.sliderOnOwnClick.bind(this);
10 | this.thumbOnOwnMouseMove = this.thumbOnOwnMouseMove.bind(this);
11 | }
12 |
13 | shouldComponentUpdate() {
14 | return false;
15 | }
16 |
17 | componentDidMount() {
18 | this.sliderHeight = this.slider.offsetHeight;
19 | this.thumbHeight = this.thumb.offsetHeight;
20 | this._cfg = {
21 | MIN: 0,
22 | MAX: this.slider.offsetHeight - this.thumb.offsetHeight
23 | };
24 |
25 | let thumbTop = (this.props.initVal - 1) * this._cfg.MAX;
26 |
27 | this.thumb.style.top = thumbTop + 'px';
28 | this.counter.style.height = this._cfg.MAX - thumbTop + this.thumbHeight / 2 + 'px';
29 | }
30 |
31 | thumbOnOwnMouseMove(e) {
32 | let {handler, type, title} = this.props,
33 | shiftY = e.pageY - getCoords(this.thumb).top;
34 |
35 | document.onmousemove = (e) => {
36 | let move = e.pageY - getCoords(this.slider).top - shiftY,
37 | ratio = 1 - move / this._cfg.MAX;
38 |
39 | if (ratio <= 0) {
40 | ratio = 0;
41 | move = this._cfg.MAX;
42 | } else if (ratio >= 1) {
43 | ratio = 1;
44 | move = 0;
45 | }
46 |
47 | this.thumb.style.top = move + 'px';
48 | this.counter.style.height = this._cfg.MAX - move + this.thumbHeight / 2 + 'px';
49 |
50 | handler({title, type, ratio});
51 |
52 | document.onmouseup = (e) => {
53 | document.onmousemove = document.onmouseup = null;
54 | };
55 |
56 | };
57 | }
58 |
59 | sliderOnOwnClick(e) {
60 | if (e.target == this.thumb) return;
61 |
62 | let {handler, type, title} = this.props,
63 | move = e.pageY - getCoords(this.slider).top - this.thumbHeight / 2,
64 | ratio = 1 - move / this._cfg.MAX;
65 |
66 | if (ratio <= 0) {
67 | ratio = 0;
68 | move = this._cfg.MAX;
69 | } else if (ratio >= 1) {
70 | ratio = 1;
71 | move = 0;
72 | }
73 |
74 | this.thumb.style.top = move + 'px';
75 | this.counter.style.height = this._cfg.MAX - move + this.thumbHeight / 2 + 'px';
76 |
77 | handler({title, type, ratio});
78 | }
79 |
80 | render() {
81 | return (
82 |
83 |
this.slider = s}>
86 |
this.thumb = th}>
90 |
91 |
this.counter = c}>
93 |
94 |
95 |
96 | );
97 | }
98 |
99 | static propTypes = {
100 | handler: React.PropTypes.func.isRequired,
101 | type: React.PropTypes.string.isRequired,
102 | title: React.PropTypes.string.isRequired,
103 | initVal: React.PropTypes.number.isRequired
104 | }
105 | }
106 |
107 | module.exports = VerticalSliderItem;
--------------------------------------------------------------------------------
/dev/js/constants/dataConstants.js:
--------------------------------------------------------------------------------
1 | const UPDATE_DATA = 'UPDATE_DATA';
2 |
3 | module.exports = {
4 | UPDATE_DATA
5 | };
--------------------------------------------------------------------------------
/dev/js/constants/favoritesConstants.js:
--------------------------------------------------------------------------------
1 | const FETCH_FAVORITES_START = 'FETCH_FAVORITES_START',
2 | UPDATE_FAVORITES = 'UPDATE_FAVORITES',
3 | FETCH_FAVORITES_ERROR = 'FETCH_FAVORITES_ERROR',
4 | UPDATE_AMOUNT = 'UPDATE_AMOUNT',
5 | REMOVE_TRACK = 'REMOVE_TRACK',
6 | SHUFFLE_TRACKS = 'SHUFFLE_TRACKS';
7 |
8 | module.exports = {
9 | FETCH_FAVORITES_START,
10 | UPDATE_FAVORITES,
11 | FETCH_FAVORITES_ERROR,
12 | UPDATE_AMOUNT,
13 | REMOVE_TRACK,
14 | SHUFFLE_TRACKS
15 | };
--------------------------------------------------------------------------------
/dev/js/constants/playerConstants.js:
--------------------------------------------------------------------------------
1 | const TOGGLE_TRACK = 'TOGGLE_TRACK',
2 | CHANGE_CURRENT_TIME = 'CHANGE_CURRENT_TIME',
3 | CHANGE_PLAYING_TRACK = 'CHANGE_PLAYING_TRACK',
4 | NEXT_TRACK = 'NEXT_TRACK',
5 | PREV_TRACK = 'PREV_TRACK',
6 | TOGGLE_REPEAT_TRACK = 'TOGGLE_REPEAT_TRACK';
7 |
8 | module.exports = {
9 | TOGGLE_TRACK,
10 | CHANGE_CURRENT_TIME,
11 | CHANGE_PLAYING_TRACK,
12 | NEXT_TRACK,
13 | PREV_TRACK,
14 | TOGGLE_REPEAT_TRACK
15 | };
--------------------------------------------------------------------------------
/dev/js/constants/playlistConstants.js:
--------------------------------------------------------------------------------
1 | const FETCH_PLAYLIST_START = 'FETCH_PLAYLIST_START',
2 | UPDATE_PLAYLIST = 'UPDATE_PLAYLIST',
3 | FETCH_PLAYLIST_ERROR = 'FETCH_PLAYLIST_ERROR',
4 | UPDATE_SEARCH_TEXT = 'UPDATE_SEARCH_TEXT',
5 | SHUFFLE_TRACKS = 'SHUFFLE_TRACKS',
6 | CONCAT_PARTIAL_TRACKS = 'CONCAT_PARTIAL_TRACKS';
7 |
8 | module.exports = {
9 | FETCH_PLAYLIST_START,
10 | UPDATE_PLAYLIST,
11 | FETCH_PLAYLIST_ERROR,
12 | UPDATE_SEARCH_TEXT,
13 | SHUFFLE_TRACKS,
14 | CONCAT_PARTIAL_TRACKS
15 | };
--------------------------------------------------------------------------------
/dev/js/constants/uiConstants.js:
--------------------------------------------------------------------------------
1 | const CHANGE_CURRENT_TAB = 'CHANGE_CURRENT_TAB';
2 |
3 | module.exports = {
4 | CHANGE_CURRENT_TAB
5 | };
--------------------------------------------------------------------------------
/dev/js/containers/ActionBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | playerAPI = require('../utils/playerAPI'),
4 | PlayButton = require('../components/buttons/PlayButton'),
5 | NextButton = require('../components/buttons/NextButton'),
6 | PrevButton = require('../components/buttons/PrevButton');
7 |
8 | class ActionBar extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.prevBtnOnClick = this.prevBtnOnClick.bind(this);
13 | this.playBtnOnClick = this.playBtnOnClick.bind(this);
14 | this.nextBtnOnClick = this.nextBtnOnClick.bind(this);
15 | }
16 |
17 | prevBtnOnClick() {
18 | playerAPI.playPrev();
19 | }
20 |
21 | playBtnOnClick() {
22 | playerAPI.toggle();
23 | }
24 |
25 | nextBtnOnClick() {
26 | playerAPI.playNext();
27 | }
28 |
29 | render() {
30 | return (
31 |
38 | );
39 | }
40 |
41 | static propTypes = {
42 | isPlaying: React.PropTypes.bool.isRequired
43 | }
44 | }
45 |
46 | function mapStateToProps(state) {
47 | return {
48 | isPlaying: state.player.isPlaying
49 | };
50 | }
51 |
52 | module.exports = connect(mapStateToProps)(ActionBar);
--------------------------------------------------------------------------------
/dev/js/containers/App.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | soundcloud = require('soundcloud'),
3 | {connect} = require('react-redux'),
4 | playerAPI = require('../utils/playerAPI'),
5 | localStore = require('../utils/localStore'),
6 | {updateAmount} = require('../actions/favoritesActions');
7 |
8 | const ControlBar = require('./../components/ControlBar'),
9 | Header = require('./../components/Header'),
10 | Content = require('./../components/Content');
11 |
12 | class App extends React.Component {
13 |
14 | shouldComponentUpdate() {
15 | return false;
16 | }
17 |
18 | componentDidMount() {
19 | let {dispatch} = this.props;
20 |
21 | playerAPI.canvasSpectrum._initCanvas(document.querySelector('#canvas'));
22 |
23 | soundcloud.initialize({
24 | client_id: '94b8a7e5efe62b01c8ca3f03cc3ccca8'
25 | });
26 |
27 | dispatch(updateAmount(localStore.length));
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | static propTypes = {
43 | favorites: React.PropTypes.object.isRequired,
44 | currentTab: React.PropTypes.string.isRequired
45 | }
46 | }
47 |
48 | function mapStateToProps(state) {
49 | return {
50 | favorites: state.favorites,
51 | currentTab: state.ui.currentTab
52 | };
53 | }
54 |
55 | module.exports = connect(mapStateToProps)(App);
--------------------------------------------------------------------------------
/dev/js/containers/Artwork.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | Cover = require('../components/Cover');
4 |
5 | class Artwork extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | loading: false
12 | };
13 |
14 | this.coverOnLoad = this.coverOnLoad.bind(this);
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | this.setState({
19 | loading: this.props.src !== nextProps.src
20 | });
21 | }
22 |
23 | coverOnLoad() {
24 | this.setState({
25 | loading: false
26 | });
27 | }
28 |
29 | render() {
30 | return (
31 |
32 | );
33 | }
34 |
35 | static propTypes = {
36 | src: React.PropTypes.string.isRequired
37 | }
38 | }
39 |
40 | function mapStateToProps(state) {
41 | return {
42 | src: state.data.cover || 'http://placehold.it/300x300/fff/3cd2ce/?text=REACT.JS+PLAYER'
43 | };
44 | }
45 |
46 | module.exports = connect(mapStateToProps)(Artwork);
--------------------------------------------------------------------------------
/dev/js/containers/Playlist.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | PlaylistComponent = require('../components/Playlist'),
4 | {playTrack} = require('../actions/playerActions'),
5 | {updateData} = require('../actions/dataActions'),
6 | {changeCurrentTab} = require('../actions/uiActions'),
7 | {concatPartialTracks, fetchPlaylistStart, fetchPlaylistError} = require('../actions/playlistActions'),
8 | {fetchFavoritesStart, updateFavorites, fetchFavoritesError, updateAmount, removeTrack} = require('../actions/favoritesActions'),
9 | playerAPI = require('../utils/playerAPI'),
10 | localStore = require('../utils/localStore');
11 |
12 | class Playlist extends React.Component {
13 |
14 | constructor(props) {
15 | super(props);
16 |
17 | this.trackOnClick = this.trackOnClick.bind(this);
18 | this.tabOnChanged = this.tabOnChanged.bind(this);
19 | this.starOnClick = this.starOnClick.bind(this);
20 | this.trackListOnScrolled = this.trackListOnScrolled.bind(this);
21 | }
22 |
23 | trackOnClick(e, trackData) {
24 | if (e.target.dataset.val) return;
25 |
26 | const {dispatch, playlist, currentTab} = this.props;
27 |
28 | dispatch(playTrack(trackData.index));
29 |
30 | dispatch(updateData(Object.assign(trackData, {
31 | currentPlaylist: currentTab,
32 | searchText: playlist.searchText
33 | })));
34 |
35 | playerAPI.play(trackData.streamUrl);
36 | }
37 |
38 | starOnClick(e, trackData) {
39 | const {dispatch, currentTab} = this.props;
40 |
41 | if (currentTab === 'favorites') {
42 | localStore.remove(trackData);
43 | dispatch(removeTrack(trackData.id));
44 | dispatch(updateAmount(localStore.length));
45 | } else {
46 | localStore.add(trackData);
47 | dispatch(updateAmount(localStore.length));
48 | }
49 | }
50 |
51 | trackListOnScrolled(e, cb) {
52 | const {dispatch, playlist: {searchText, done}} = this.props;
53 |
54 | !done && dispatch((dispatch) => {
55 |
56 | dispatch(fetchPlaylistStart());
57 |
58 | playerAPI.findPartialTracks(searchText)
59 | .then(tracks => {
60 |
61 | dispatch(concatPartialTracks(tracks));
62 |
63 | cb(e);
64 | })
65 | .catch(error => {
66 | dispatch(fetchPlaylistError(error))
67 | });
68 | });
69 |
70 | }
71 |
72 | tabOnChanged(tabName) {
73 | const {dispatch, favorites} = this.props;
74 |
75 | if (tabName === 'favorites') {
76 | if (favorites.amount > favorites.tracks.length) {
77 | dispatch(fetchFavoritesStart());
78 |
79 | playerAPI.findTracksByIds(localStore.store[localStore._value])
80 | .then(tracks => {
81 | dispatch(updateFavorites(tracks));
82 | })
83 | .catch(error => fetchFavoritesError(error));
84 | }
85 | }
86 |
87 | dispatch(changeCurrentTab(tabName));
88 | }
89 |
90 | render() {
91 | const {playlist, currentTab, favorites} = this.props;
92 | const fetching = currentTab === 'playlist' ? playlist.fetching : favorites.fetching;
93 |
94 | return (
95 |
103 | );
104 | }
105 |
106 | static propTypes = {
107 | playlist: React.PropTypes.object.isRequired,
108 | data: React.PropTypes.object.isRequired,
109 | currentTrackIndex: React.PropTypes.number,
110 | currentTab: React.PropTypes.string.isRequired,
111 | favorites: React.PropTypes.object.isRequired
112 | }
113 | }
114 |
115 | function mapStateToProps(state) {
116 | return {
117 | playlist: state.playlist,
118 | data: state.data,
119 | currentTrackIndex: state.player.currentTrackIndex,
120 | currentTab: state.ui.currentTab,
121 | favorites: state.favorites
122 | };
123 | }
124 |
125 | module.exports = connect(mapStateToProps)(Playlist);
--------------------------------------------------------------------------------
/dev/js/containers/ProcessingBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | playerAPI = require('../utils/playerAPI'),
4 | VolumeBar = require('../components/VolumeBar'),
5 | EqualizerBar = require('../components/Equalizer'),
6 | {shuffleTracks: shufflePlaylist} = require('../actions/playlistActions'),
7 | {shuffleTracks: shuffleFavorites} = require('../actions/favoritesActions');
8 |
9 | class ProcessingBar extends React.Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.setVolume = this.setVolume.bind(this);
14 | this.toggleRepeat = this.toggleRepeat.bind(this);
15 | this.shuffleCurrentPlaylist = this.shuffleCurrentPlaylist.bind(this);
16 | this.filterFrequencies = this.filterFrequencies.bind(this);
17 | }
18 |
19 | setVolume(volume) {
20 | playerAPI.setVolume(volume);
21 | }
22 |
23 | filterFrequencies(filter) {
24 | playerAPI.filterValues(filter);
25 | }
26 |
27 | toggleRepeat() {
28 | playerAPI.toggleRepeat();
29 | }
30 |
31 | shuffleCurrentPlaylist() {
32 | let {dispatch, currentTab} = this.props;
33 |
34 | currentTab === 'playlist' ? dispatch(shufflePlaylist()) : dispatch(shuffleFavorites());
35 | }
36 |
37 | render() {
38 | let repeat = this.props.repeat;
39 |
40 | return (
41 |
49 | );
50 | }
51 |
52 | static propTypes = {
53 | repeat: React.PropTypes.bool.isRequired,
54 | currentTab: React.PropTypes.string.isRequired
55 | }
56 | }
57 |
58 | function mapStateToProps(state) {
59 | return {
60 | repeat: state.player.repeat,
61 | currentTab: state.ui.currentTab
62 | };
63 | }
64 |
65 | module.exports = connect(mapStateToProps)(ProcessingBar);
66 |
--------------------------------------------------------------------------------
/dev/js/containers/SearchBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | playerAPI = require('../utils/playerAPI'),
4 | {fetchPlaylistStart, updatePlaylist, fetchPlaylistError, changeSearchText} = require('../actions/playlistActions');
5 |
6 | let lastTimeValue = 0,
7 | timer = null;
8 |
9 | class SearchBar extends React.Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | value: '',
15 | visible: false
16 | };
17 |
18 | this.inputOnChange.bind(this);
19 | this.clearInputField.bind(this);
20 | }
21 |
22 | componentDidMount() {
23 | this.searchField.focus();
24 | this.searchField.value = 'Hans Zimmer';
25 | this.inputOnChange({target: this.searchField});
26 | }
27 |
28 | componentWillReceiveProps(nextProps) {
29 | if (nextProps.searchText !== this.props.searchText) {
30 | this.searchField.value = nextProps.searchText;
31 | this.inputOnChange({target: this.searchField});
32 | }
33 | }
34 |
35 | inputOnChange(e) {
36 | let val = e.target.value,
37 | newTimeValue = new Date(),
38 | delay = this.props.searchDelay,
39 | {dispatch} = this.props;
40 |
41 | if ((newTimeValue - lastTimeValue) < delay) {
42 | clearTimeout(timer);
43 | }
44 |
45 | timer = setTimeout(function () {
46 |
47 | dispatch(changeSearchText(val));
48 |
49 | dispatch((dispatch) => {
50 | dispatch(fetchPlaylistStart());
51 |
52 | playerAPI.findTracks(val)
53 | .then(tracks => {
54 | dispatch(updatePlaylist(tracks));
55 | })
56 | .catch(error => {
57 | dispatch(fetchPlaylistError(error));
58 | });
59 |
60 | });
61 | }, delay);
62 |
63 | this.setState({
64 | value: val,
65 | visible: (val.length ? true : false)
66 | });
67 |
68 | lastTimeValue = newTimeValue;
69 | }
70 |
71 | clearInputField() {
72 | clearTimeout(timer);
73 |
74 | this.setState({
75 | value: '',
76 | visible: false
77 | });
78 |
79 | this.searchField.focus();
80 | }
81 |
82 | render() {
83 | let {visible, value} = this.state;
84 |
85 | return (
86 |
87 |
88 |
89 |
this.searchField = sf}
94 | />
95 |
97 |
98 |
99 |
100 | );
101 | }
102 |
103 | static propTypes = {
104 | searchText: React.PropTypes.string.isRequired,
105 | searchDelay: React.PropTypes.number.isRequired
106 | }
107 | }
108 |
109 | function mapStateToProps(state) {
110 | return {
111 | searchText: state.playlist.searchText
112 | };
113 | }
114 |
115 | module.exports = connect(mapStateToProps)(SearchBar);
--------------------------------------------------------------------------------
/dev/js/containers/TimeBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | DurationBar = require('../components/DurationBar'),
4 | CurrentTimeBar = require('../components/CurrentTimeBar'),
5 | ProgressBar = require('../components/ProgressBar'),
6 | playerAPI = require('../utils/playerAPI');
7 |
8 | class TimeBar extends React.Component {
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | this.updateTime = this.updateTime.bind(this);
14 | }
15 |
16 | updateTime(time) {
17 | let duration = this.props.duration;
18 |
19 | playerAPI.currentTime = parseInt(time * duration);
20 |
21 | playerAPI.startOver();
22 | }
23 |
24 | render() {
25 | let {currentTime, duration} = this.props,
26 | ratio = currentTime / duration;
27 |
28 | return (
29 |
36 | );
37 | }
38 |
39 | static propTypes = {
40 | currentTime: React.PropTypes.number.isRequired,
41 | duration: React.PropTypes.number.isRequired
42 | }
43 | }
44 |
45 | function mapStateToProps(state) {
46 | return {
47 | currentTime: state.player.currentTime || 0,
48 | duration: state.data.duration || 0
49 | };
50 | }
51 |
52 | module.exports = connect(mapStateToProps)(TimeBar);
--------------------------------------------------------------------------------
/dev/js/containers/TrackData.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | {connect} = require('react-redux'),
3 | ReactCSSTransitionGroup = require('react-addons-css-transition-group');
4 |
5 | class TrackData extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 |
10 | this.titleOnMouseOver = this.titleOnMouseOver.bind(this);
11 | this.titleOnMouseOut = this.titleOnMouseOut.bind(this);
12 | }
13 |
14 | titleOnMouseOver(e) {
15 | let title = e.target;
16 |
17 | if (title.offsetWidth < title.scrollWidth) {
18 | title.style.textIndent = -(title.scrollWidth - title.offsetWidth) + 'px';
19 | }
20 | }
21 |
22 | titleOnMouseOut(e) {
23 | let title = e.target;
24 | title.style.transition = 'text-indent .7s';
25 | title.style.textIndent = '0px';
26 | }
27 |
28 | render() {
29 | let {title, username} = this.props;
30 |
31 | return (
32 |
33 |
34 |
this.titleOnMouseOver(e)}
36 | onMouseOut={(e) => this.titleOnMouseOut(e)}
37 | className="track-data__title"
38 | ref={(t) => this.title = t}>
39 | {title}
40 |
41 |
42 | {username}
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | static propTypes = {
50 | username: React.PropTypes.string,
51 | title: React.PropTypes.string
52 | }
53 |
54 | }
55 | function mapStateToProps(state) {
56 | let {username, title} = state.data;
57 |
58 | return {
59 | username,
60 | title
61 | };
62 | }
63 |
64 | module.exports = connect(mapStateToProps)(TrackData);
--------------------------------------------------------------------------------
/dev/js/containers/VoiceBar.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | annyang = require('annyang'),
3 | commands = require('../utils/voiceCommands');
4 |
5 | class VoiceBar extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | isActive: false
11 | };
12 |
13 | if (annyang) {
14 | annyang.addCommands(commands);
15 | }
16 |
17 | this.voiceBarOnClick = this.voiceBarOnClick.bind(this);
18 | }
19 |
20 | componentWillUpdate(nextProps, nextState) {
21 | if (nextState.isActive) {
22 | annyang.start();
23 | } else {
24 | annyang.abort();
25 | }
26 | }
27 |
28 | voiceBarOnClick() {
29 | this.setState({
30 | isActive: !this.state.isActive
31 | });
32 | }
33 |
34 | render() {
35 | return (
36 |
42 | )
43 | }
44 | }
45 |
46 | module.exports = VoiceBar;
--------------------------------------------------------------------------------
/dev/js/index.js:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | ReactDOM = require('react-dom'),
3 | {Provider} = require('react-redux'),
4 | App = require('./containers/App'),
5 | store = require('./reducers/store');
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
--------------------------------------------------------------------------------
/dev/js/reducers/dataReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = require('./initialState'),
2 | constants = require('../constants/dataConstants');
3 |
4 | function dataReducer(state = initialState.data, action) {
5 | switch (action.type) {
6 | case constants.UPDATE_DATA:
7 | return Object.assign({}, state, action.data);
8 | default:
9 | return state;
10 | }
11 | }
12 |
13 | module.exports = dataReducer;
--------------------------------------------------------------------------------
/dev/js/reducers/factory.js:
--------------------------------------------------------------------------------
1 | const formatCover = require('../utils/format').formatCover;
2 |
3 | class Track {
4 | constructor({title, index, artwork_url : cover, stream_url : streamUrl, id, duration, uri, user : {username}}) {
5 | Object.assign(this, {
6 | title,
7 | index,
8 | cover: formatCover(cover),
9 | id,
10 | duration,
11 | uri,
12 | streamUrl,
13 | username
14 | });
15 | }
16 | }
17 |
18 | const entity = {
19 | track: Track
20 | };
21 |
22 | module.exports = {
23 | createEntity: function (type, props) {
24 | return new entity[type](props);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/dev/js/reducers/favoritesReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = require('./initialState'),
2 | constants = require('../constants/favoritesConstants'),
3 | factory = require('./factory'),
4 | shuffle = require('../utils/format').shuffle,
5 | normalizeIndices = require('../utils/format').normalizeIndices;
6 |
7 | // artwork_url is a property of a SC track object that represents a cover
8 |
9 | function favoritesReducer(state = initialState.favorites, action) {
10 | switch (action.type) {
11 | case constants.FETCH_FAVORITES_START:
12 | return Object.assign({}, state, {
13 | fetched: false,
14 | fetching: true
15 | });
16 |
17 | case constants.UPDATE_FAVORITES:
18 | return Object.assign({}, state, {
19 | fetching: false,
20 | fetched: true,
21 | tracks: action.tracks
22 | .filter(track => track.artwork_url && track.streamable)
23 | .map((track, index) => factory.createEntity('track', Object.assign(track, {
24 | index: index
25 | })))
26 | });
27 |
28 | case constants.FETCH_FAVORITES_ERROR:
29 | return Object.assign({}, state, {
30 | fetching: false,
31 | error: action.error
32 | });
33 |
34 | case constants.REMOVE_TRACK:
35 | return Object.assign({}, state, {
36 | tracks: state.tracks.filter(track => track.id !== action.id)
37 | });
38 |
39 | case constants.UPDATE_AMOUNT:
40 | return Object.assign({}, state, {
41 | amount: action.amount
42 | });
43 |
44 | case constants.SHUFFLE_TRACKS:
45 | return Object.assign({}, state, {
46 | tracks: normalizeIndices(shuffle(state.tracks))
47 | });
48 |
49 | default:
50 | return state;
51 | }
52 | }
53 |
54 | module.exports = favoritesReducer;
--------------------------------------------------------------------------------
/dev/js/reducers/initialState.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | playlist: {
3 | tracks: [],
4 | searchText: '',
5 | error: null,
6 | fetching: false,
7 | fetched: false,
8 | done: false
9 | },
10 | favorites: {
11 | tracks: [],
12 | amount: 0,
13 | error: null,
14 | fetching: false,
15 | fetched: false
16 | },
17 | player: {
18 | currentTrackIndex: null,
19 | currentTime: 0,
20 | isPlaying: false,
21 | repeat: false
22 | },
23 | data: {
24 | currentPlaylist: '',
25 | duration: '',
26 | index: '',
27 | cover: '',
28 | username: '',
29 | title: '',
30 | id: ''
31 | },
32 | ui: {
33 | currentTab: 'playlist'
34 | }
35 | };
36 |
37 | module.exports = initialState;
--------------------------------------------------------------------------------
/dev/js/reducers/playerReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = require('./initialState'),
2 | constants = require('../constants/playerConstants');
3 |
4 | function playerReducer(state = initialState.player, action) {
5 |
6 | switch (action.type) {
7 | case constants.CHANGE_PLAYING_TRACK:
8 | return Object.assign({}, state, {
9 | currentTrackIndex: action.trackIndex
10 | });
11 |
12 | case constants.TOGGLE_TRACK:
13 | return Object.assign({}, state, {
14 | isPlaying: action.isPlaying
15 | });
16 |
17 | case constants.CHANGE_CURRENT_TIME:
18 | return Object.assign({}, state, {
19 | currentTime: action.time
20 | });
21 |
22 | case constants.TOGGLE_REPEAT_TRACK:
23 | return Object.assign({}, state, {
24 | repeat: !state.repeat
25 | });
26 |
27 | default:
28 | return state;
29 | }
30 | }
31 |
32 | module.exports = playerReducer;
--------------------------------------------------------------------------------
/dev/js/reducers/playlistReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = require('./initialState'),
2 | constants = require('../constants/playlistConstants'),
3 | factory = require('./factory'),
4 | shuffle = require('../utils/format').shuffle,
5 | normalizeIndices = require('../utils/format').normalizeIndices;
6 |
7 | // artwork_url is a property of a SC track object that represents a cover
8 |
9 | function playerReducer(state = initialState.playlist, action) {
10 | switch (action.type) {
11 | case constants.FETCH_PLAYLIST_START:
12 | return Object.assign({}, state, {
13 | fetched: false,
14 | fetching: true
15 | });
16 |
17 | case constants.UPDATE_PLAYLIST:
18 | return Object.assign({}, state, {
19 | fetching: false,
20 | fetched: true,
21 | tracks: action.tracks
22 | .filter(track => track.artwork_url && track.streamable)
23 | .map((track, index) => factory.createEntity('track', Object.assign(track, {
24 | index: index
25 | })))
26 | });
27 |
28 | case constants.FETCH_PLAYLIST_ERROR:
29 | return Object.assign({}, state, {
30 | fetching: false,
31 | error: action.error
32 | });
33 |
34 | case constants.UPDATE_SEARCH_TEXT:
35 | return Object.assign({}, state, {
36 | searchText: action.text,
37 | done: false
38 | });
39 |
40 | case constants.CONCAT_PARTIAL_TRACKS:
41 | let lastIndex = state.tracks[state.tracks.length - 1].index + 1;
42 | let newTracks = action.tracks.collection
43 | .filter(track => track.artwork_url && track.streamable)
44 | .map((track, index) => factory.createEntity('track', Object.assign(track, {
45 | index: index + lastIndex
46 | })));
47 |
48 | return Object.assign({}, state, {
49 | fetching: false,
50 | fetched: true,
51 | tracks: state.tracks.concat(newTracks),
52 | done: !action.tracks.next_href
53 | });
54 |
55 | case constants.SHUFFLE_TRACKS:
56 | return Object.assign({}, state, {
57 | tracks: normalizeIndices(shuffle(state.tracks))
58 | });
59 |
60 | default:
61 | return state;
62 | }
63 | }
64 |
65 | module.exports = playerReducer;
66 |
--------------------------------------------------------------------------------
/dev/js/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | const playlistReducer = require('./playlistReducer'),
2 | playerReducer = require('./playerReducer'),
3 | dataReducer = require('./dataReducer'),
4 | uiReducer = require('./uiReducer'),
5 | favoritesReducer = require('./favoritesReducer'),
6 | initialState = require('./initialState');
7 |
8 | function reducer(state = initialState, action) {
9 | let playlist = playlistReducer(state.playlist, action),
10 | player = playerReducer(state.player, action),
11 | data = dataReducer(state.data, action),
12 | favorites = favoritesReducer(state.favorites, action),
13 | ui = uiReducer(state.ui, action);
14 |
15 | return {
16 | playlist,
17 | player,
18 | data,
19 | favorites,
20 | ui
21 | };
22 | }
23 |
24 | module.exports = reducer;
--------------------------------------------------------------------------------
/dev/js/reducers/store.js:
--------------------------------------------------------------------------------
1 | const rootReducer = require('./rootReducer'),
2 | {createStore, applyMiddleware} = require('redux'),
3 | thunk = require('redux-thunk').default;
4 | // logger = require('redux-logger')();
5 |
6 | const middlewares = [thunk];
7 |
8 | const middleWare = applyMiddleware(...middlewares);
9 | const store = createStore(rootReducer, middleWare);
10 |
11 | module.exports = store;
--------------------------------------------------------------------------------
/dev/js/reducers/uiReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = require('./initialState'),
2 | constants = require('../constants/uiConstants');
3 |
4 | function uiReducer(store = initialState.ui, action) {
5 | switch (action.type) {
6 | case constants.CHANGE_CURRENT_TAB:
7 | return Object.assign({}, store, {
8 | currentTab: action.tab
9 | });
10 | default:
11 | return store;
12 | }
13 | }
14 |
15 | module.exports = uiReducer;
16 |
--------------------------------------------------------------------------------
/dev/js/utils/CanvasSpectrum.js:
--------------------------------------------------------------------------------
1 | class CanvasSpectrum {
2 |
3 | _initCanvas(canvas) {
4 | if (!canvas) {
5 | throw new TypeError("Failed to execute '_initCanvas()': at least 1 argument required")
6 | }
7 |
8 | this.canvas = canvas;
9 | this._context = canvas.getContext('2d');
10 |
11 | this._context.lineWidth = 4;
12 |
13 | this._gradStyle = CanvasSpectrum.createLinearGradient(this._context);
14 | }
15 |
16 | draw(arr) {
17 | this.clear();
18 |
19 | this._context.strokeStyle = this._gradStyle();
20 |
21 | this._context.beginPath();
22 |
23 | for (let i = 0; i < arr.length; i++) {
24 | let y = arr[i] / 128 * this.canvas.height / 2;
25 |
26 | if (i === 0) {
27 | this._context.moveTo(0, y / 2)
28 | } else {
29 | this._context.lineTo(i + 2, y / 2);
30 | }
31 | }
32 |
33 | this._context.stroke();
34 | this._context.closePath();
35 | }
36 |
37 | clear() {
38 | this._context.clearRect(0, 0, this.canvas.width, this.canvas.height);
39 | }
40 |
41 | update(arr) {
42 | this.draw(arr);
43 | }
44 |
45 | static createLinearGradient(context) {
46 | let grad = context.createLinearGradient(0, 0, context.canvas.width, 0),
47 | prevColorPattern = 'rgba(60, 210, 206, .3)';
48 |
49 | return function () {
50 | let color1 = parseInt(Math.random() * 255),
51 | color2 = parseInt(Math.random() * 255),
52 | color3 = parseInt(Math.random() * 255);
53 |
54 | let nextColorPattern = `rgb(${color1}, ${color2}, ${color3})`;
55 |
56 | grad.addColorStop(1, prevColorPattern);
57 | grad.addColorStop(0, nextColorPattern);
58 |
59 | return grad;
60 | }
61 | }
62 |
63 | }
64 |
65 | module.exports = CanvasSpectrum;
--------------------------------------------------------------------------------
/dev/js/utils/WebAudioAnalyzer.js:
--------------------------------------------------------------------------------
1 | window.AudioContext = window.AudioContext || window.webkitAudioContext;
2 |
3 | class Analyzer {
4 | constructor() {
5 | this._ctx = new AudioContext();
6 | this._node = this._ctx.createScriptProcessor(2048, 1, 1);
7 |
8 | this._mid = this._ctx.createBiquadFilter();
9 | this._mid.type = "peaking";
10 | this._mid.frequency.value = 350;
11 | this._mid.gain.value = 0;
12 |
13 | this._bass = this._ctx.createBiquadFilter();
14 | this._bass.type = "lowshelf";
15 | this._bass.frequency.value = 200;
16 | this._bass.gain.value = 0;
17 |
18 | this._treble = this._ctx.createBiquadFilter();
19 | this._treble.type = "highshelf";
20 | this._treble.frequency.value = 3000;
21 | this._treble.gain.value = 0;
22 |
23 | this._analyzer = this._ctx.createAnalyser();
24 | this._analyzer.smoothingTimeConstant = 0.5;
25 | this._analyzer.fftSize = 1024;
26 |
27 | this._bands = new Uint8Array(this._analyzer.frequencyBinCount);
28 |
29 | this._cfg = {
30 | MAX_GAIN: 3,
31 | MIN_GAIN: -3,
32 | MAX_TREBLE_VALUE: 6000,
33 | MAX_BASS_VALUE: 400,
34 | MAX_MID_VALUE: 700
35 | };
36 |
37 | window.SC.initialize({
38 | client_id: '94b8a7e5efe62b01c8ca3f03cc3ccca8'
39 | });
40 | }
41 |
42 | filterValues(filter) {
43 | // KEK (:
44 | let title = filter.title === 'BASS' ? '_bass' : filter.title === 'MID' ? '_mid' : '_treble';
45 |
46 | this[title][filter.type].value = filter.type === 'frequency' ?
47 | filter.ratio * this._cfg[`MAX_${filter.title}_VALUE`] :
48 | -1 * (3 + ((3 * (filter.ratio * (-3))) / (3 / 2)));
49 | }
50 |
51 | _initSource(input, updateHandler) {
52 | this.audio = input;
53 |
54 | if (!this._source) {
55 | this._source = this._ctx.createMediaElementSource(this.audio);
56 | }
57 |
58 | this._source.connect(this._bass);
59 | this._bass.connect(this._mid);
60 | this._mid.connect(this._treble);
61 | this._treble.connect(this._analyzer);
62 | this._analyzer.connect(this._node);
63 | this._node.connect(this._ctx.destination);
64 | this._analyzer.connect(this._ctx.destination);
65 |
66 | this._node.onaudioprocess = () => {
67 | if (!this.audio.paused) {
68 | this._analyzer.getByteFrequencyData(this._bands);
69 |
70 | updateHandler(this._bands);
71 | }
72 | };
73 | };
74 | }
75 |
76 | module.exports = Analyzer;
--------------------------------------------------------------------------------
/dev/js/utils/dom.js:
--------------------------------------------------------------------------------
1 | exports.getCoords = function (elem) {
2 | const box = elem.getBoundingClientRect();
3 |
4 | const body = document.body;
5 | const docElem = document.documentElement;
6 |
7 | const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
8 | const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
9 |
10 | const clientTop = docElem.clientTop || body.clientTop || 0;
11 | const clientLeft = docElem.clientLeft || body.clientLeft || 0;
12 |
13 | const top = box.top + scrollTop - clientTop;
14 | const left = box.left + scrollLeft - clientLeft;
15 |
16 | return {
17 | top: Math.round(top),
18 | left: Math.round(left)
19 | };
20 | };
--------------------------------------------------------------------------------
/dev/js/utils/format.js:
--------------------------------------------------------------------------------
1 | const url = require('url');
2 |
3 | exports.formatMStoS = function (ms) {
4 | let num = ms / 1000,
5 | result;
6 |
7 | if (num < 60) {
8 | result = num < 10 ? '0:0' + parseInt(num) : '0:' + parseInt(num);
9 | } else if (num > 60) {
10 | let div = parseInt(num / 60),
11 | dif = parseInt(num) - (60 * div),
12 | mins = div < 10 ? '0' + div : num,
13 | secs = dif < 10 ? '0' + dif : dif;
14 |
15 | result = mins + ':' + secs;
16 | }
17 |
18 | return result;
19 | };
20 |
21 | exports.formatCover = function (coverUri, size = 't300x300', format = '.png') {
22 | const {host, protocol, pathname, search} = url.parse(coverUri);
23 |
24 | return protocol + '//' + host + pathname.split('.')[0].split('-').slice(0, -1).join('-') + '-' + size + format + (search || '');
25 | };
26 |
27 | exports.formatTitle = function (title, lim = 50) {
28 | return title.length >= lim ? (title.substr(0, lim - 3) + '...') : title
29 | };
30 |
31 | exports.shuffle = function (arr) {
32 | let a = arr.slice(),
33 | j, x, i;
34 |
35 | for (i = a.length; i; i--) {
36 | j = Math.floor(Math.random() * i);
37 | x = a[i - 1];
38 | a[i - 1] = a[j];
39 | a[j] = x;
40 | }
41 |
42 | return a;
43 | };
44 |
45 | exports.normalizeIndices = function (arr) {
46 | let i;
47 |
48 | for (i = 0; i < arr.length; i++) {
49 | arr[i].index = i;
50 | }
51 |
52 | return arr;
53 | };
--------------------------------------------------------------------------------
/dev/js/utils/localStore.js:
--------------------------------------------------------------------------------
1 | const {getState, dispatch} = require('../reducers/store'),
2 | {updateFavoriteTracks} = require('../actions/playlistActions'),
3 | playerAPI = require('./playerAPI');
4 |
5 | class LocalStore {
6 | constructor() {
7 | this.store = window.localStorage;
8 |
9 | this._value = 'rrsap16';
10 |
11 | if (!this.store[this._value]) {
12 | this.store[this._value] = '';
13 | }
14 | }
15 |
16 | add(track) {
17 | if (this.store[this._value].indexOf(track.id) >= 0) {
18 | this.remove(track);
19 | return;
20 | }
21 |
22 | if (!this.store[this._value].length) {
23 | this.store[this._value] += track.id;
24 | return;
25 | }
26 |
27 | this.store[this._value] += ',' + track.id;
28 | }
29 |
30 | includes(id) {
31 | return this.store[this._value].split(',').includes(id.toString());
32 | }
33 |
34 | get length() {
35 | let arr = this.store[this._value].split(',');
36 |
37 | if (arr[0] === '') {
38 | return 0;
39 | }
40 |
41 | return arr.length
42 | }
43 |
44 | remove(track) {
45 | if (this.store[this._value].indexOf(track.id) < 0) return;
46 |
47 | this.store[this._value] = this.store[this._value].split(',').filter(trackId => trackId !== track.id.toString()).join(',');
48 | }
49 | }
50 |
51 | const store = new LocalStore();
52 |
53 | module.exports = store;
--------------------------------------------------------------------------------
/dev/js/utils/playerAPI.js:
--------------------------------------------------------------------------------
1 | const soundcloud = require('soundcloud'),
2 | url = require('url'),
3 | Analyzer = require('./WebAudioAnalyzer'),
4 | CanvasSpectrum = require('./CanvasSpectrum'),
5 | {getState, dispatch} = require('../reducers/store'),
6 | {changeCurrentTime, changeTrack, toggleTrack, playTrack, toggleRepeatTrack} = require('../actions/playerActions'),
7 | {updateData} = require('../actions/dataActions'),
8 | {getCurrentTrackObject, getFirstTrack} = require('../utils/trackUtils'),
9 | {NEXT_TRACK, PREV_TRACK} = require('../constants/playerConstants');
10 |
11 | class PlayerAPI {
12 | constructor(clientId = '94b8a7e5efe62b01c8ca3f03cc3ccca8') {
13 | this.audio = new Audio();
14 | this.audio.crossOrigin = "anonymous";
15 |
16 | this.clientId = clientId;
17 |
18 | window.SC.initialize({
19 | client_id: this.clientId
20 | });
21 |
22 | this.audioAnalyzer = new Analyzer();
23 | this.canvasSpectrum = new CanvasSpectrum(document.getElementById('canvas'));
24 | this.nextHref = null;
25 | this.repeat = false;
26 |
27 | this.audio.addEventListener('ended', () => this.audioOnFinish());
28 | this.audio.addEventListener('timeupdate', () => this.audioOnTimeUpdate());
29 | this.audio.addEventListener('canplaythrough', () => this.audioOnCanPlayThrough());
30 | }
31 |
32 | findTracks(searchString) {
33 | this.nextHref = null;
34 |
35 | return soundcloud.get('/tracks', PlayerAPI.createQueryObject(searchString, true))
36 | .then(result => {
37 | this.nextHref = url.parse(result.next_href, true).query;
38 |
39 | return result.collection;
40 | });
41 | }
42 |
43 | filterValues(filter) {
44 | this.audioAnalyzer.filterValues(filter);
45 | }
46 |
47 | toggleRepeat() {
48 | dispatch(toggleRepeatTrack());
49 | this.repeat = !this.repeat;
50 | }
51 |
52 | findPartialTracks(searchString) {
53 | if (this.nextHref === null) return;
54 |
55 | return soundcloud.get('/tracks', this.nextHref)
56 | .then(result => {
57 | this.nextHref = result.next_href ? url.parse(result.next_href, true).query : result.next_href;
58 |
59 | return result;
60 | });
61 | }
62 |
63 | findTrackById(id) {
64 | return soundcloud.get(`/tracks/${id}`);
65 | }
66 |
67 | findTracksByIds(ids) {
68 | return soundcloud.get('/tracks?ids=' + ids);
69 | }
70 |
71 | loadTrack(src) {
72 | this.audio.src = src + '?client_id=' + this.clientId;
73 | }
74 |
75 | playNext() {
76 | if (this.audio.src) {
77 | dispatch(changeTrack(NEXT_TRACK));
78 | this.play(getCurrentTrackObject().streamUrl);
79 |
80 | dispatch(updateData(getCurrentTrackObject()));
81 | }
82 | }
83 |
84 | playPrev() {
85 | if (this.audio.src) {
86 | dispatch(changeTrack(PREV_TRACK));
87 | this.play(getCurrentTrackObject().streamUrl);
88 |
89 | dispatch(updateData(getCurrentTrackObject()));
90 | }
91 | }
92 |
93 | play(src) {
94 | if (this.audio.src === src) return;
95 |
96 | this.loadTrack(src);
97 |
98 | dispatch(toggleTrack(true));
99 |
100 | this.audio.play();
101 | }
102 |
103 | pause() {
104 | this.audio.pause();
105 |
106 | dispatch(toggleTrack(false));
107 | }
108 |
109 | startOver() {
110 | this.audio.play();
111 |
112 | dispatch(toggleTrack(true));
113 | }
114 |
115 | setVolume(val) {
116 | this.audio.volume = val;
117 | }
118 |
119 | playFirstTrack() {
120 | let track = getFirstTrack(),
121 | {ui, playlist} = getState();
122 |
123 | dispatch(playTrack(track.index));
124 |
125 | dispatch(updateData(Object.assign(track, {
126 | currentPlaylist: ui.currentTab,
127 | searchText: playlist.searchText
128 | })));
129 |
130 | this.play(track.streamUrl);
131 | }
132 |
133 | toggle() {
134 | if (!this.audio.currentSrc) {
135 | this.playFirstTrack();
136 | return;
137 | }
138 |
139 | if (this.audio.paused) {
140 | this.startOver();
141 | } else {
142 | this.pause();
143 | }
144 | }
145 |
146 | get currentTime() {
147 | return this.audio.currentTime * 1000;
148 | }
149 |
150 | set currentTime(time) {
151 | this.audio.currentTime = time / 1000;
152 | }
153 |
154 | audioOnCanPlayThrough() {
155 | this.audioAnalyzer._initSource(this.audio, this.canvasSpectrum.update.bind(this.canvasSpectrum));
156 | }
157 |
158 | audioOnTimeUpdate() {
159 | let time = this.currentTime;
160 |
161 | dispatch(changeCurrentTime(time));
162 | }
163 |
164 | audioOnFinish() {
165 | if (this.repeat) {
166 | setTimeout(() => {
167 | dispatch(changeCurrentTime(0));
168 | this.currentTime = 0;
169 | this.startOver();
170 | }, 500);
171 |
172 | return;
173 | }
174 |
175 | this.playNext();
176 | }
177 |
178 | static createQueryObject(searchString, partial = false) {
179 | let query = {
180 | q: searchString,
181 | limit: 200
182 | };
183 |
184 | return partial ? Object.assign(query, {linked_partitioning: 1}) : query;
185 | }
186 | }
187 |
188 | const playerAPI = new PlayerAPI();
189 |
190 | module.exports = playerAPI;
--------------------------------------------------------------------------------
/dev/js/utils/trackUtils.js:
--------------------------------------------------------------------------------
1 | const {getState} = require('../reducers/store');
2 |
3 | exports.getCurrentTrackObject = function () {
4 | const state = getState(),
5 | index = state.player.currentTrackIndex,
6 | currentPlaylist = state.data.currentPlaylist || state.ui.currentTab;
7 |
8 | return state[currentPlaylist].tracks[index];
9 | };
10 |
11 | exports.getFirstTrack = function () {
12 | const state = getState(),
13 | currentPlaylist = state.ui.currentTab || state.data.currentPlaylist;
14 |
15 | return state[currentPlaylist].tracks[0];
16 | };
--------------------------------------------------------------------------------
/dev/js/utils/voiceCommands.js:
--------------------------------------------------------------------------------
1 | const playerAPI = require('./playerAPI'),
2 | {dispatch, getState} = require('../reducers/store'),
3 | favoritesActions = require('../actions/favoritesActions'),
4 | playlistActions = require('../actions/playlistActions'),
5 | {changeCurrentTab} = require('../actions/uiActions'),
6 | localStore = require('./localStore');
7 |
8 | const voiceCommands = {
9 | 'switch': function () {
10 | playerAPI.toggle();
11 | },
12 | 'play next track': function () {
13 | playerAPI.playNext();
14 | },
15 | 'play previous track': function () {
16 | playerAPI.playPrev();
17 | },
18 | 'repeat track': function () {
19 | playerAPI.toggleRepeat();
20 | },
21 | 'search for *track': function (track) {
22 | dispatch(playlistActions.changeSearchText(track));
23 | },
24 | 'play playlist': function () {
25 | dispatch(changeCurrentTab('playlist'));
26 | playerAPI.playFirstTrack();
27 | },
28 | 'play favorites': function () {
29 | let {favorites} = getState();
30 |
31 | if (favorites.amount > favorites.tracks.length) {
32 | dispatch(favoritesActions.fetchFavoritesStart());
33 | dispatch(changeCurrentTab('favorites'));
34 | playerAPI.findTracksByIds(localStore.store[localStore._value])
35 | .then(tracks => {
36 | dispatch(favoritesActions.updateFavorites(tracks));
37 | playerAPI.playFirstTrack();
38 | })
39 | .catch(error => favoritesActions.fetchFavoritesError(error));
40 | } else {
41 | dispatch(changeCurrentTab('favorites'));
42 | playerAPI.playFirstTrack();
43 | }
44 | },
45 | 'shuffle': function () {
46 | let {currentTab} = getState().ui;
47 |
48 | if (currentTab === 'playlist') {
49 | dispatch(playlistActions.shuffleTracks());
50 | } else {
51 | dispatch(favoritesActions.shuffleTracks());
52 | }
53 | }
54 | };
55 |
56 | module.exports = voiceCommands;
57 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp'),
2 | autoprefixer = require('gulp-autoprefixer'),
3 | concat = require('gulp-concat'),
4 | babel = require('gulp-babel'),
5 | cleanCSS = require('gulp-clean-css'),
6 | source = require('vinyl-source-stream'),
7 | buffer = require('vinyl-buffer'),
8 | uglify = require('gulp-uglify'),
9 | rename = require('gulp-rename'),
10 | browserify = require('browserify'),
11 | browserSync = require('browser-sync').create(),
12 | del = require('del'),
13 | babelify = require('babelify');
14 |
15 | const isProduction = (() => {
16 | const isProd = process.env.NODE_ENV === 'production';
17 |
18 | console.log(`Running in ${isProd ? 'production' : 'development'} mode...`);
19 |
20 | return process.env.NODE_ENV === 'production';
21 | })();
22 |
23 | const VENDORS = [
24 | 'react',
25 | 'react-dom',
26 | 'react-addons-css-transition-group',
27 | 'react-redux',
28 | 'redux',
29 | 'url',
30 | 'soundcloud',
31 | 'react-spinner'
32 | ];
33 |
34 | gulp.task('clear:public', function () {
35 | return del(['public/js/*.js', 'public/css/*.css']);
36 | });
37 |
38 | gulp.task('styles', function () {
39 |
40 | gulp.src('dev/fonts/**.*')
41 | .pipe(gulp.dest('public/fonts'));
42 |
43 | return gulp.src('dev/**/*.css')
44 | .pipe(autoprefixer())
45 | .pipe(cleanCSS({compatibility: 'ie8'}))
46 | .pipe(gulp.dest('public'));
47 | });
48 |
49 | gulp.task('build:vendor', function () {
50 |
51 | if (isProduction) {
52 | return browserify()
53 | .require(VENDORS)
54 | .bundle()
55 | .pipe(source('vendor.min.js'))
56 | .pipe(buffer())
57 | .pipe(uglify({
58 | mangle: false,
59 | compress: true
60 | }))
61 | .pipe(gulp.dest('public/js/'));
62 | } else {
63 | return browserify()
64 | .require(VENDORS)
65 | .bundle()
66 | .pipe(source('vendor.js'))
67 | .pipe(gulp.dest('public/js'));
68 | }
69 | });
70 |
71 | gulp.task('js', function () {
72 | const props = {
73 | entries: ['dev/js/index.js'],
74 | cache: {},
75 | packageCache: {},
76 | transform: [babelify.configure({
77 | presets: ["es2015", "react", "stage-2"]
78 | })]
79 | };
80 |
81 | if (isProduction) {
82 | return browserify(props)
83 | .external(VENDORS)
84 | .bundle()
85 | .pipe(source('bundle.min.js'))
86 | .pipe(buffer())
87 | .pipe(uglify({
88 | mangle: false,
89 | compress: true
90 | }))
91 | .pipe(gulp.dest('public/js'));
92 | } else {
93 | return browserify(props)
94 | .external(VENDORS)
95 | .bundle()
96 | .pipe(source('bundle.js'))
97 | .pipe(gulp.dest('public/js'));
98 | }
99 | });
100 |
101 | gulp.task('scripts', function () {
102 | const src = isProduction ? gulp.src(['public/js/vendor.min.js', 'public/js/bundle.min.js']) :
103 | gulp.src(['public/js/vendor.js', 'public/js/bundle.js']);
104 |
105 | return src
106 | .pipe(concat('all.js'))
107 | .pipe(gulp.dest('public/js'));
108 | });
109 |
110 | gulp.task('serve', function () {
111 | browserSync.init({
112 | server: 'public'
113 | });
114 |
115 | browserSync.watch('public/**/*.*').on('change', browserSync.reload);
116 | });
117 |
118 | gulp.task('watch', function () {
119 | gulp.watch('dev/css/*.css', gulp.series('styles'));
120 | gulp.watch('dev/js/**/*.js', gulp.series('js'));
121 | });
122 |
123 | gulp.task('build', gulp.series(gulp.parallel('js', 'styles', 'build:vendor'), 'scripts'));
124 |
125 | gulp.task('dev', gulp.series('clear:public', 'build', gulp.parallel('watch', 'serve')));
126 | gulp.task('prod', gulp.series('clear:public', 'build'));
127 |
128 | gulp.task('default', gulp.series('prod'));
129 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React.js voice audio player
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-voice-audio-player",
3 | "version": "1.0.0",
4 | "description": "A functional react.js audio player",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/abitlog/react-redux-voice-audio-player.git"
9 | },
10 | "dependencies": {
11 | "annyang": "^2.5.0",
12 | "react": "^15.3.0",
13 | "react-dom": "^15.3.0",
14 | "react-addons-css-transition-group": "^15.3.1",
15 | "react-redux": "^4.4.5",
16 | "react-spinner": "^0.2.6",
17 | "redux": "^3.5.2",
18 | "soundcloud": "^3.1.2",
19 | "url": "^0.11.0"
20 | },
21 | "devDependencies": {
22 | "babel-preset-es2015": "^6.14.0",
23 | "babel-preset-react": "^6.11.1",
24 | "babel-preset-stage-2": "^6.13.0",
25 | "babelify": "^7.3.0",
26 | "browser-sync": "^2.14.0",
27 | "browserify": "^13.1.0",
28 | "del": "^2.2.1",
29 | "gulp": "github:gulpjs/gulp#4.0",
30 | "gulp-autoprefixer": "^3.1.0",
31 | "gulp-babel": "^6.1.2",
32 | "gulp-clean-css": "^2.0.12",
33 | "gulp-concat": "^2.6.0",
34 | "gulp-notify": "^2.2.0",
35 | "gulp-rename": "^1.2.2",
36 | "gulp-sass": "^2.3.2",
37 | "gulp-uglify": "^1.5.4",
38 | "redux-devtools": "^3.3.1",
39 | "redux-logger": "^2.6.1",
40 | "redux-thunk": "^2.1.0",
41 | "vinyl-buffer": "^1.0.0",
42 | "vinyl-source-stream": "^1.1.0"
43 | },
44 | "scripts": {
45 | "start": "NODE_ENV=production gulp",
46 | "dev": "NODE_ENV=development gulp dev"
47 | },
48 | "keywords": [
49 | "react",
50 | "redux",
51 | "audio",
52 | "audioplayer",
53 | "soundcloud",
54 | "react-redux",
55 | "voice",
56 | "player"
57 | ],
58 | "author": "abitlog",
59 | "license": "MIT"
60 | }
61 |
--------------------------------------------------------------------------------
/public/css/styles.css:
--------------------------------------------------------------------------------
1 | *,.icon{-webkit-font-smoothing:antialiased}.icon,body{line-height:1}.artwork,body{box-sizing:border-box}#player,#player__visual-bar,.artwork,.playlist__item,.track-list{position:relative}.track-data__title,.track-title{white-space:nowrap;overflow:hidden}#content,#player,.track-data__title,.track-list,.track-title{overflow:hidden}a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;vertical-align:baseline}.action-bar li,.content-container,.processing-bar li,.time-bar li{vertical-align:middle}article,aside,details,figcaption,figure,footer,header,menu,nav,section{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0}@font-face{font-family:IcoMoon-Free;src:url(../fonts/IcoMoon-Free.ttf) format('truetype');font-weight:400;font-style:normal}.icon{color:#666;font-size:1.1em;font-family:IcoMoon-Free!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;display:inline-block;cursor:pointer;letter-spacing:0;-webkit-font-feature-settings:"liga";-ms-font-feature-settings:"liga" 1;-o-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale}body{-webkit-font-smoothing:subpixel-antialiased}div,img,li{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}:active,:focus{outline:0}.container{margin-right:auto;margin-left:auto;padding-left:6px;padding-right:6px}.container:after,.container:before{content:" ";display:table}.container:after{clear:both}#player{width:700px;margin:30px auto 0;border-radius:4px;background-color:#fff;z-index:2;border:1px solid gray}#player__visual-bar{width:100%;height:340px;overflow:hidden;z-index:1}.artwork,.artwork__img,.visual-bar-right-header,.voice-bar{height:100%}.artwork{float:left;width:100%;padding:10px 40px 30px}.artwork__img{width:100%;max-width:100%;border-radius:3px}.visual-bar-right{float:left;width:50%}.visual-bar-left-header,.visual-bar-right-header{width:100%;padding:15px 30px;box-sizing:border-box}.track-data{width:85%;float:left;box-sizing:border-box}.voice-bar{width:15%;float:right;text-align:right;box-sizing:border-box}.voice-bar__controller{width:50%;text-align:center;-webkit-transition:color .5s;transition:color .5s}.voice-bar__controller:hover{color:#3cd2ce}.voice-bar__controller::after{content:'\e91e';width:100%;height:100%}.track-data__title{font-family:'Hind Siliguri',sans-serif;font-weight:700;font-size:.9em;color:#5e5e5e;-webkit-transition:text-indent 1.5s;transition:text-indent 1.5s;cursor:default}.search-bar__input,.track-data__username{font-family:'Open Sans Condensed',sans-serif}.track-data__username{font-size:.85em;color:#5e5e5e;padding-top:4px}.search-bar{width:75%;margin:0 auto;border:1px solid rgba(0,0,0,.1);border-radius:15px}.search-bar__magnifier-sign{width:10%;display:inline-block;font-size:13px;text-align:right;cursor:default}.search-bar__magnifier-sign::before{content:'\e986'}.search-bar__input{font-size:.9em;width:80%;border-radius:5px;border:0;padding:5px;box-sizing:border-box}.hide{display:none!important}.search-bar__x-sign{width:10%;display:inline-block;font-size:9px;-webkit-transform:translate(4px,-2px);transform:translate(4px,-2px);color:rgba(0,0,0,.2);cursor:default}.search-bar__x-sign::before{content:'\ea0f'}#header{height:60px;width:100%}.header__left{width:50%;float:left;height:100%}.header__right{width:50%;float:right;height:100%}.track-index,.track-title{float:left;box-sizing:border-box}.visual-bar-right-body{position:relative;clear:both;width:100%;height:100%;z-index:1}.track-list{height:229px;background-color:rgba(227,227,227,.4);box-sizing:border-box}.currentTrack,.playlist__item:hover{box-shadow:0 0 5px rgba(60,210,206,.5);background-color:rgba(60,210,206,.4)}.playlist__item{font-family:'Hind Siliguri',sans-serif;color:#555;font-size:.78em;border-top:1px solid rgba(227,227,227,.7);padding:11px 10px 11px 30px;-webkit-transition:all .4s;transition:all .4s;box-sizing:border-box}.playlist__item:first-child{border-top:none}.playlist__item:hover{cursor:default}.track-title{width:87%;-webkit-transition:text-indent 2s;transition:text-indent 2s}.track-index{padding-right:7px;width:5%;font-size:9px;color:#3cd2ce;font-weight:700}.track-star{padding:0 4px;color:rgba(102,102,102,.4)}.track-unstar{padding:0 4px;color:rgba(163,12,7,.4)}.track-star::after{content:'\ea0a';font-size:.8em;text-align:center;box-sizing:border-box;-webkit-transition:color .3s,content .5s;transition:color .3s,content .5s;visibility:hidden}.track-star__favorite::after{content:'\ea0f'!important;font-size:.8em!important;color:rgba(163,12,7,.7)!important;text-align:center;box-sizing:border-box;-webkit-transition:color .3s,content .5s;transition:color .3s,content .5s;visibility:hidden}.playlist__item:hover .track-star::after{visibility:visible}.list-scroller{position:absolute;top:0;right:2px;width:4px;height:80px;background-color:rgba(85,85,85,.3);border-radius:4px;cursor:default;z-index:1000;-webkit-transition:background-color .4s,top .1s;transition:background-color .4s,top .1s;visibility:hidden}.list-scroller:hover{background-color:rgba(85,85,85,.5)}.clearfix:after,.clearfix:before{content:"";display:table}.clearfix:after{clear:both}.clearfix{zoom:1}#control-bar{position:relative;clear:both;padding:20px 30px;box-shadow:0 -1px 20px rgba(0,0,0,.3);z-index:1}.control-bar__content{width:100%;max-width:100%;display:table}.action-bar{display:table-cell;width:15%}.action-bar ul{width:100%;display:table}.action-bar li{display:table-cell;text-align:center;width:33%}.action-bar li.prev::before{content:"\ea1f"}.action-bar li.stop::before{content:"\ea1d"}.action-bar li.play::before{content:"\ea1c"}.action-bar li.next::before{content:"\ea20"}.time-bar{display:table-cell;width:60%}.time-bar ul{width:100%;display:table}.time-bar li{display:table-cell;width:80%;text-align:center}.time-bar li:first-child,.time-bar li:last-child{font-family:tahoma,arial,verdana,sans-serif,Lucida Sans;font-size:.65em;color:#555;width:10%}.player-tooltip__title--medium,.vertical-slider-wrapper__title-wrapper{font-size:.6em}.time-bar li:first-child{text-align:right}.time-bar li:last-child{text-align:left}.time-bar__progress-bar .progress-slider{width:90%}.progress-slider{display:inline-block;position:relative;height:6px;cursor:pointer;box-shadow:0 0 0 #000,0 0 0 #0d0d0d;background:#ddd;border-radius:4px;border:0 solid rgba(0,0,0,0);z-index:10}.progress-slider>.progress-slider__thumb{position:absolute;left:0;box-shadow:0 0 1px #000,0 0 1px #0d0d0d;border:4px solid #fff;height:5px;width:5px;border-radius:50%;background:#3cd2ce;cursor:pointer;margin-top:-3.5px;z-index:100}.progress-slider__timeCounter{position:absolute;height:6px;width:3px;left:0;top:0;background-color:#3cd2ce;border-radius:4px;z-index:5}.player-tooltip--medium,.player-tooltip--small{box-shadow:0 6px 25px 1px rgba(0,0,0,.2);border-radius:6px}.processing-bar{display:table-cell;width:25%}.processing-bar ul{width:100%;display:table}.processing-bar li{position:relative;display:table-cell;width:25%;text-align:center}.processing-bar li.volume-high::before{content:"\ea26"}.processing-bar li.volume-medium::before{content:"\ea27"}.processing-bar li.volume-low::before{content:"\ea28"}.processing-bar li.volume-mute::before{content:"\ea2a"}.processing-bar li.equalizer::before{content:"\e993"}#processing-bar__shuffle::before{content:"\ea30"}#repeat::before{content:"\ea2e"}.progress-slider__time-pointer{display:inline-block;position:absolute;background-color:#000;width:6px;height:6px}.player-tooltip{visibility:hidden;background-color:#fff;text-align:center;position:absolute;left:50%;top:-78%;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%);z-index:1;cursor:default;-webkit-transition:opacity .8s;transition:opacity .8s}.player-tooltip--medium{padding:10px}.player-tooltip--small{padding:5px}.player-tooltip::after{content:'';position:absolute;top:100%;left:50%}.vertical-slider-wrapper{display:inline-block;width:100%}.vertical-slider-wrapper__slider-wrapper{display:inline-block;width:13px}.player-tooltip__title,.vertical-slider-wrapper__title-wrapper{color:#bfbfbf;width:100%;font-weight:700;margin-top:5px;font-family:'Hind Siliguri',sans-serif;text-align:center}.player-tooltip--medium::after{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #fff;margin-left:-8px}.player-tooltip--small::after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #fff;margin-left:-6px}.player-tooltip__title--small{font-size:.4em}.vertical-slider{display:inline-block;position:relative;height:80px;width:5px;box-shadow:0 0 0 #000,0 0 0 #0d0d0d;background:#ddd;border-radius:4px;border:0 solid rgba(0,0,0,0);z-index:10}.vertical-slider__thumb{position:absolute;left:0;top:0;box-shadow:0 0 1px #000,0 0 1px #0d0d0d;border:3px solid #fff;height:4px;width:4px;border-radius:50%;background:#3cd2ce;cursor:pointer;margin-left:-2px;z-index:100}.vertical-slider__timeCounter{position:absolute;height:6px;width:5px;left:0;bottom:0;background-color:#3cd2ce;border-radius:4px;z-index:5}.activated{color:#3ccecf;z-index:1000}.vol-title{font-family:'Hind Siliguri',sans-serif;font-weight:700;margin-top:5px;font-size:.6em;color:#bfbfbf;width:100%}.vol-title span{text-align:center}.content-container{font-family:'Hind Siliguri',sans-serif;color:#5e5e5e;margin:0 auto;text-align:center;width:60%;height:70%}#content{position:relative;width:100%;height:263px}.content__left,.content__right{position:relative;width:50%;height:100%;float:left}.content__right{z-index:1}.action-text{font-size:1.5em}.file-b-lg{display:inline-block;font-size:1.2em;padding:7px 14px;border:1px solid grey;border-radius:3px;margin:15px 0;cursor:pointer}.react-spinner{position:absolute;width:45px;height:45px;top:50%;left:50%}.react-spinner_bar{-webkit-animation:react-spinner_spin 1.2s linear infinite;animation:react-spinner_spin 1.2s linear infinite;border-radius:5px;background-color:#fff;border:1px solid rgba(53,63,77,.29);position:absolute;width:20%;height:7.8%;top:-3.9%;left:-10%}#canvas{position:absolute;left:calc(50% - 260px/2);bottom:0;text-align:center}.control-bar__content li.icon{-webkit-transition:color .3s,-webkit-transform .6s;transition:color .3s,-webkit-transform .6s;transition:color .3s,transform .6s;transition:color .3s,transform .6s,-webkit-transform .6s}.control-bar__content li.icon:hover{color:#3ccecf}.playlist-menu{height:auto;width:100%;box-sizing:border-box}.playlist-menu input[type=radio]{display:none}.favorites-tab,.playlist-tab{clear:both;position:relative}.playlist-menu__tab{font-family:'Open Sans Condensed',sans-serif;font-size:.8em;display:block;width:50%;float:left;padding:10px;text-align:center;box-sizing:border-box;cursor:pointer;-webkit-transition:color .3s;transition:color .3s}.playlist-menu__tab span{position:relative;font-size:.6em;left:2px;top:-5px}.playlist-menu input[type=radio]:checked+label{font-size:.9em;color:#3cd2ce;background-color:rgba(227,227,227,.4)}.inactive{display:none}.activatedBlock{opacity:0;-webkit-transition:opacity 1s;transition:opacity 1s}.fade-enter{opacity:.01}.fade-enter.fade-enter-active{opacity:1;-webkit-transition:opacity .5s ease-in;transition:opacity .5s ease-in}.fade-leave{opacity:1}.fade-leave.fade-leave-active{opacity:.01;-webkit-transition:opacity .3s ease-in;transition:opacity .3s ease-in}.fade-appear{opacity:.01}.fade-appear.fade-appear-active{opacity:1;-webkit-transition:opacity .5s ease-in;transition:opacity .5s ease-in}@keyframes react-spinner_spin{0%{opacity:1}100%{opacity:.15}}@-webkit-keyframes react-spinner_spin{0%{opacity:1}100%{opacity:.15}}@media (min-width:768px){.container{width:732px}}@media (min-width:992px){.container{width:952px}}@media (min-width:1200px){.container{width:1152px}}
--------------------------------------------------------------------------------
/public/fonts/IcoMoon-Free.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grobjin9/react.js-voice-audio-player/d57f06b0d7cfd3497e0b66c65775fa6cce4eb8f4/public/fonts/IcoMoon-Free.ttf
--------------------------------------------------------------------------------
/public/fonts/Read Me.txt:
--------------------------------------------------------------------------------
1 | In this folder, you can find the IcoMoon-Free font in TTF format. You can install this font so that you can use it in desktop applications.
2 |
3 | Open "Reference.html" to see a list of the icons available in this font. The text box located to the bottom right of each icon contains a character (which may be invisible). You can copy and use this character in any desktop application that allows entering text and choosing a custom font to display it. You may also type or copy the text in the "liga" field if the environment in which you're using the font supports ligatures.
4 |
5 | To get crisp results, use font sizes that are a multiple of 16px.
6 |
7 | It is not recommend to use this font on the web. To make an optimized webfont, use the IcoMoon app (https://icomoon.io/app). This app allows you to choose the icons that you need and make them into webfonts.
8 |
9 | You can import "selection.json" to the IcoMoon app to modify this font.
10 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Functional React.js + Redux.js Voice Audio Player
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------