├── .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 | ![alt tag](demo.gif) 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 |
11 |
12 | 13 | 14 | 15 |
16 |
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 | 18 | 19 | 26 | 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 | 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 |