audio player

Add a audio player to listen audio without leaving current page.

目前为 2023-04-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name audio player
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.41
  5. // @description Add a audio player to listen audio without leaving current page.
  6. // @author oshibuki
  7. // @match https://t-nagano.com/projects/JapaneseGenki3rdEdAudio/
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=t-nagano.com
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15. // Your CSS code here
  16. var style = document.createElement('style');
  17. style.type = "text/css";
  18. var content = `
  19. *,
  20. *::before,
  21. *::after {
  22. box-sizing: border-box;
  23. }
  24.  
  25. body {
  26. margin: 0;
  27. padding: 0;
  28. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  29. font-size: 14px;
  30. }
  31.  
  32. pre, code {
  33. font-family: Consolas, Courier, monospace;
  34. font-size: inherit;
  35. color: #333;
  36. background: #fafafa;
  37. }
  38.  
  39. pre {
  40. padding: 1rem;
  41. border: 1px solid #eee;
  42. overflow: auto;
  43. }
  44.  
  45. .container-fluid {
  46. padding-bottom:50px;
  47. }
  48.  
  49. /*-----------------------
  50. Audio Player - AP
  51. ------------------------*/
  52. .ap {
  53. position: fixed;
  54. left: 0;
  55. right: 0;
  56. bottom: 0;
  57. width: 100%;
  58. height: 50px;
  59. font-family: inherit;
  60. font-size: 14px;
  61. -webkit-user-select: none;
  62. -moz-user-select: none;
  63. -ms-user-select: none;
  64. user-select: none;
  65. border-top: 1px solid #ccc;
  66. background: #f2f2f2;
  67. box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
  68. z-index: 99999;
  69. }
  70.  
  71. .ap__inner {
  72. display: flex;
  73. max-width: 1440px;
  74. margin: auto;
  75. }
  76.  
  77. .ap__item {
  78. display: flex;
  79. flex: 1;
  80. justify-content: center;
  81. align-items: center;
  82. }
  83.  
  84. .ap__item--playback > .ap__controls,
  85. .ap__item--settings > .ap__controls {
  86. flex: 0 25%;
  87. }
  88.  
  89. @-webkit-keyframes fs {
  90. 0% {
  91. opacity: 0;
  92. transform: scale(0.5);
  93. }
  94. 100% {
  95. opacity: 1;
  96. transform: scale(1);
  97. }
  98. }
  99.  
  100. @keyframes fs {
  101. 0% {
  102. opacity: 0;
  103. transform: scale(0.5);
  104. }
  105. 100% {
  106. opacity: 1;
  107. transform: scale(1);
  108. }
  109. }
  110. .ap__item--track {
  111. flex: 1 40%;
  112. padding: 0 20px;
  113. }
  114.  
  115. .track {
  116. position: relative;
  117. width: 100%;
  118. align-self: flex-start;
  119. padding: 5px 0 0;
  120. }
  121.  
  122. .track__title {
  123. position: absolute;
  124. width: 100%;
  125. overflow: hidden;
  126. padding-right: 80px;
  127. text-align: left;
  128. white-space: nowrap;
  129. text-overflow: ellipsis;
  130. }
  131.  
  132. .track__time {
  133. position: absolute;
  134. top: 5px;
  135. right: 0;
  136. }
  137.  
  138. .progress-container {
  139. position: relative;
  140. padding: 7px 0;
  141. margin-top: 15px;
  142. overflow: hidden;
  143. cursor: pointer;
  144. }
  145. .progress-container:hover .progress__bar:after {
  146. opacity: 1;
  147. }
  148.  
  149. .progress {
  150. height: 3px;
  151. border-radius: 3px;
  152. background: #ddd;
  153. }
  154.  
  155. .progress__bar,
  156. .progress__preload {
  157. position: absolute;
  158. width: 0;
  159. height: 3px;
  160. border-radius: 3px 0 0 3px;
  161. }
  162.  
  163. .progress__bar {
  164. background: steelblue;
  165. z-index: 1;
  166. }
  167. .progress__bar:after {
  168. position: absolute;
  169. top: 0;
  170. right: -10px;
  171. width: 10px;
  172. height: 10px;
  173. margin-top: -3px;
  174. content: "";
  175. border-radius: 6px;
  176. background: steelblue;
  177. opacity: 0;
  178. transition: opacity 0.3s ease;
  179. }
  180.  
  181. .progress__bar--active:after {
  182. transform: scale(1.4);
  183. }
  184.  
  185. .progress__preload {
  186. background: #c4c4c4;
  187. z-index: 0;
  188. }
  189.  
  190. .ap__controls,
  191. .ap button {
  192. margin: 0;
  193. padding: 0;
  194. border: 0;
  195. outline: 0;
  196. background: transparent;
  197. position: relative;
  198. display: block;
  199. height: 50px;
  200. text-align: center;
  201. cursor: pointer;
  202. transition: background 0.3s ease;
  203. }
  204. .ap__controls:active,
  205. .ap button:active {
  206. background: rgba(0, 0, 0, 0.1);
  207. }
  208. .ap__controls:hover,
  209. .ap button:hover {
  210. opacity: 1;
  211. }
  212. .ap__controls--playlist {
  213. display: none;
  214. }
  215. .icon-play > path {
  216. transition: all 0.3s ease;
  217. }
  218.  
  219. .is-playing .icon-play {
  220. fill: steelblue;
  221. }
  222.  
  223. .volume-btn {
  224. display: block;
  225. text-align: center;
  226. width: 100%;
  227. }
  228.  
  229. .volume {
  230. position: absolute;
  231. left: 50%;
  232. bottom: 45px;
  233. width: 40px;
  234. margin-left: -20px;
  235. height: 120px;
  236. opacity: 0;
  237. visibility: hidden;
  238. transform: translateY(10px);
  239. transition: all 0.3s cubic-bezier(0.17, 0.72, 0.26, 1.23);
  240. background: #f2f2f2;
  241. border: 1px solid #ccc;
  242. border-radius: 1px;
  243. z-index: 88888;
  244. }
  245. .volume::before, .volume::after {
  246. content: "";
  247. position: absolute;
  248. bottom: -12px;
  249. border: 7px solid transparent;
  250. border-top: 7px solid #f2f2f2;
  251. left: 50%;
  252. margin-left: -7px;
  253. }
  254. .volume::after {
  255. bottom: -14px;
  256. z-index: -1;
  257. border-top: 7px solid #ccc;
  258. }
  259.  
  260. .volume-container:hover .volume {
  261. opacity: 1;
  262. transform: translateY(0);
  263. visibility: visible;
  264. }
  265.  
  266. .volume__track {
  267. position: relative;
  268. display: block;
  269. width: 3px;
  270. height: 100px;
  271. margin: 10px auto;
  272. background: #ddd;
  273. border-radius: 3px;
  274. overflow: hidden;
  275. }
  276.  
  277. .volume__bar {
  278. position: absolute;
  279. left: 0;
  280. right: 0;
  281. bottom: 0;
  282. background: steelblue;
  283. height: 50%;
  284. }
  285.  
  286. .icon-volume-off {
  287. display: none;
  288. }
  289.  
  290. .has-muted .icon-volume-on {
  291. display: none;
  292. }
  293. .has-muted .icon-volume-off {
  294. display: inline;
  295. opacity: 0.7;
  296. }
  297.  
  298. .ap__controls.is-active > svg {
  299. fill: steelblue;
  300. filter: drop-shadow(0 0 3px rgba(70, 130, 180, 0.4));
  301. }
  302.  
  303. @media (max-width: 1024px) {
  304. .ap__item > .ap__controls {
  305. flex: 1;
  306. }
  307. }
  308. @media (max-width: 580px) {
  309. .ap {
  310. min-width: 250px;
  311. }
  312.  
  313. .ap, .ap__inner {
  314. height: auto;
  315. }
  316.  
  317. .ap__inner {
  318. flex-wrap: wrap;
  319. }
  320.  
  321. .ap__item--track {
  322. margin-bottom: 10px;
  323. padding: 0 20px;
  324. order: 1;
  325. flex: 1 1 100%;
  326. }
  327.  
  328. .ap__item--playback,
  329. .ap__item--settings {
  330. flex: 1 1 50%;
  331. order: 2;
  332. }
  333. }
  334. /*-----------------------
  335. Playlist Player - PL
  336. ------------------------*/
  337. .pl-container {
  338. display: none;
  339. position: fixed;
  340. top: 0;
  341. right: 0;
  342. bottom: 50px;
  343. left: 0;
  344. overflow: auto;
  345. font-family: inherit;
  346. font-size: 14px;
  347. background: #fff;
  348. z-index: 77777;
  349. }
  350.  
  351. .pl-ul {
  352. width: 100%;
  353. max-width: 550px;
  354. margin: 0 auto;
  355. padding: 30px 10px 100px 10px;
  356. }
  357.  
  358. .pl-list {
  359. display: flex;
  360. align-items: center;
  361. height: 40px;
  362. line-height: 40px;
  363. }
  364. .pl-list svg {
  365. fill: steelblue;
  366. }
  367.  
  368. .pl-list + .pl-list {
  369. border-top: 1px solid #eee;
  370. }
  371.  
  372. .pl-list:not(.pl-list--current):hover {
  373. background: #f6f6f6;
  374. }
  375.  
  376. .pl-list__track,
  377. .pl-list__remove {
  378. flex: 0 50px;
  379. text-align: center;
  380. }
  381.  
  382. .pl-list__icon {
  383. display: inline-block;
  384. width: 0;
  385. height: 0;
  386. border-top: 5px solid transparent;
  387. border-bottom: 5px solid transparent;
  388. border-left: 8px solid #555;
  389. }
  390.  
  391. .pl-list__title {
  392. overflow: hidden;
  393. padding-right: 10px;
  394. cursor: pointer;
  395. text-align: left;
  396. white-space: nowrap;
  397. text-overflow: ellipsis;
  398. flex: 1;
  399. }
  400.  
  401. .pl-list__remove {
  402. height: 100%;
  403. background: transparent;
  404. border: 0;
  405. outline: 0;
  406. cursor: pointer;
  407. opacity: 0;
  408. transition: opacity 0.2s ease;
  409. }
  410.  
  411. .pl-list__remove > svg {
  412. width: 16px;
  413. height: 16px;
  414. }
  415.  
  416. .pl-list__eq {
  417. display: none;
  418. }
  419.  
  420. .pl-list--current {
  421. background: steelblue;
  422. color: #fff;
  423. }
  424.  
  425. .pl-list--current svg {
  426. fill: #fff;
  427. }
  428. .pl-list--current .pl-list__eq {
  429. display: block;
  430. }
  431. .pl-list--current .pl-list__icon {
  432. display: none;
  433. }
  434.  
  435. .pl-list:hover .pl-list__remove,
  436. .pl-list--current .pl-list__remove {
  437. opacity: 1;
  438. }
  439.  
  440. .pl-list--current .pl-list__remove:hover {
  441. background: #3f75a2;
  442. }
  443.  
  444. .pl-list--empty {
  445. position: absolute;
  446. top: 50%;
  447. left: 50%;
  448. font-size: 2rem;
  449. transform: translate(-50%, -50%);
  450. letter-spacing: 2px;
  451. color: #ccc;
  452. }
  453.  
  454. @-webkit-keyframes eq {
  455. 0% {
  456. height: 3px;
  457. }
  458. 50% {
  459. height: 20px;
  460. }
  461. 100% {
  462. height: 3px;
  463. }
  464. }
  465.  
  466. @keyframes eq {
  467. 0% {
  468. height: 3px;
  469. }
  470. 50% {
  471. height: 20px;
  472. }
  473. 100% {
  474. height: 3px;
  475. }
  476. }
  477. .eq {
  478. display: flex;
  479. width: 20px;
  480. height: 20px;
  481. margin: 0 auto;
  482. justify-content: space-between;
  483. align-items: flex-end;
  484. }
  485.  
  486. .eq__bar {
  487. width: 4px;
  488. background: #fff;
  489. filter: drop-shadow(0 0 5px #fff);
  490. }
  491.  
  492. .eq__bar:nth-child(1) {
  493. -webkit-animation: eq 0.8s ease-in-out infinite 0s;
  494. animation: eq 0.8s ease-in-out infinite 0s;
  495. }
  496.  
  497. .eq__bar:nth-child(2) {
  498. -webkit-animation: eq 0.8s ease-in-out infinite 0.2s;
  499. animation: eq 0.8s ease-in-out infinite 0.2s;
  500. }
  501.  
  502. .eq__bar:nth-child(3) {
  503. -webkit-animation: eq 0.8s ease-in-out infinite 0.4s;
  504. animation: eq 0.8s ease-in-out infinite 0.4s;
  505. }
  506.  
  507. .h-hide {
  508. display: none;
  509. }
  510.  
  511. .h-show {
  512. display: block;
  513. }
  514. `;
  515. if (style.styleSheet) {
  516. style.styleSheet.cssText = content;
  517. } else {
  518. style.appendChild(document.createTextNode(content));
  519. }
  520. document.head.append(style);
  521.  
  522. var myDiv = document.createElement('div');
  523. myDiv.innerHTML = `
  524. <!-- Audio player -->
  525. <div class="ap" id="ap">
  526. <div class="ap__inner">
  527. <div class="ap__item ap__item--playback">
  528. <button class="ap__controls ap__controls--prev">
  529. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  530. <path d="M9.516 12l8.484-6v12zM6 6h2.016v12h-2.016v-12z"></path>
  531. </svg>
  532. </button>
  533. <button class="ap__controls ap__controls--toggle">
  534. <svg class="icon-play" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="36" height="36" viewBox="0 0 36 36" data-play="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" data-pause="M 12,26 16.33,26 16.33,10 12,10 z M 20.66,26 25,26 25,10 20.66,10 z">
  535. <path d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z"></path>
  536. </svg>
  537. </button>
  538. <button class="ap__controls ap__controls--next">
  539. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  540. <path d="M15.984 6h2.016v12h-2.016v-12zM6 18v-12l8.484 6z"></path>
  541. </svg>
  542. </button>
  543. </div>
  544. <div class="ap__item ap__item--track">
  545. <div class="track">
  546. <div class="track__title">Queue is empty</div>
  547. <div class="track__time">
  548. <span class="track__time--current">--</span>
  549. <span> / </span>
  550. <span class="track__time--duration">--</span>
  551. </div>
  552.  
  553. <div class="progress-container">
  554. <div class="progress">
  555. <div class="progress__bar"></div>
  556. <div class="progress__preload"></div>
  557. </div>
  558. </div>
  559.  
  560. </div>
  561. </div>
  562. <div class="ap__item ap__item--settings">
  563. <div class="ap__controls volume-container">
  564. <button class="volume-btn">
  565. <svg class="icon-volume-on" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  566. <path d="M14.016 3.234q3.047 0.656 5.016 3.117t1.969 5.648-1.969 5.648-5.016 3.117v-2.063q2.203-0.656 3.586-2.484t1.383-4.219-1.383-4.219-3.586-2.484v-2.063zM16.5 12q0 2.813-2.484 4.031v-8.063q2.484 1.219 2.484 4.031zM3 9h3.984l5.016-5.016v16.031l-5.016-5.016h-3.984v-6z"></path>
  567. </svg>
  568. <svg class="icon-volume-off" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  569. <path d="M12 3.984v4.219l-2.109-2.109zM4.266 3l16.734 16.734-1.266 1.266-2.063-2.063q-1.734 1.359-3.656 1.828v-2.063q1.172-0.328 2.25-1.172l-4.266-4.266v6.75l-5.016-5.016h-3.984v-6h4.734l-4.734-4.734zM18.984 12q0-2.391-1.383-4.219t-3.586-2.484v-2.063q3.047 0.656 5.016 3.117t1.969 5.648q0 2.25-1.031 4.172l-1.5-1.547q0.516-1.266 0.516-2.625zM16.5 12q0 0.422-0.047 0.609l-2.438-2.438v-2.203q2.484 1.219 2.484 4.031z"></path>
  570. </svg>
  571. </button>
  572. <div class="volume">
  573. <div class="volume__track">
  574. <div class="volume__bar"></div>
  575. </div>
  576. </div>
  577. </div>
  578. <button class="ap__controls ap__controls--repeat">
  579. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  580. <path d="M17.016 17.016v-4.031h1.969v6h-12v3l-3.984-3.984 3.984-3.984v3h10.031zM6.984 6.984v4.031h-1.969v-6h12v-3l3.984 3.984-3.984 3.984v-3h-10.031z"></path>
  581. </svg>
  582. </button>
  583. <button class="ap__controls ap__controls--playlist" style="display:none;">
  584. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#333" width="24" height="24" viewBox="0 0 24 24">
  585. <path d="M17.016 12.984l4.969 3-4.969 3v-6zM2.016 15v-2.016h12.984v2.016h-12.984zM18.984 5.016v1.969h-16.969v-1.969h16.969zM18.984 9v2.016h-16.969v-2.016h16.969z"></path>
  586. </svg>
  587. </button>
  588. </div>
  589. </div>
  590. </div>
  591. `;
  592. var mainContent = document.getElementById('page-content-wrapper')
  593. mainContent.appendChild(myDiv);
  594.  
  595. (function(window, undefined) {
  596.  
  597. 'use strict';
  598.  
  599. var AudioPlayer = (function() {
  600.  
  601. // Player vars!
  602. var
  603. docTitle = document.title,
  604. player = document.getElementById('ap'),
  605. playBtn,
  606. playSvg,
  607. playSvgPath,
  608. prevBtn,
  609. nextBtn,
  610. plBtn,
  611. repeatBtn,
  612. volumeBtn,
  613. progressBar,
  614. preloadBar,
  615. curTime,
  616. durTime,
  617. trackTitle,
  618. audio,
  619. index = 0,
  620. playList,
  621. volumeBar,
  622. wheelVolumeValue = 0,
  623. volumeLength,
  624. repeating = false,
  625. seeking = false,
  626. seekingVol = false,
  627. rightClick = false,
  628. apActive = false,
  629. // playlist vars
  630. pl,
  631. plUl,
  632. plLi,
  633. tplList =
  634. '<li class="pl-list" data-track="{count}">'+
  635. '<div class="pl-list__track">'+
  636. '<div class="pl-list__icon"></div>'+
  637. '<div class="pl-list__eq">'+
  638. '<div class="eq">'+
  639. '<div class="eq__bar"></div>'+
  640. '<div class="eq__bar"></div>'+
  641. '<div class="eq__bar"></div>'+
  642. '</div>'+
  643. '</div>'+
  644. '</div>'+
  645. '<div class="pl-list__title">{title}</div>'+
  646. '<button class="pl-list__remove">'+
  647. '<svg fill="#000000" height="20" viewBox="0 0 24 24" width="20" xmlns="http://www.w3.org/2000/svg">'+
  648. '<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>'+
  649. '<path d="M0 0h24v24H0z" fill="none"/>'+
  650. '</svg>'+
  651. '</button>'+
  652. '</li>',
  653. // settings
  654. settings = {
  655. volume : 0.1,
  656. changeDocTitle: true,
  657. confirmClose : true,
  658. autoPlay : false,
  659. buffered : true,
  660. notification : true,
  661. playList : []
  662. };
  663.  
  664. function init(options) {
  665.  
  666. if(!('classList' in document.documentElement)) {
  667. return false;
  668. }
  669.  
  670. if(apActive || player === null) {
  671. return 'Player already init';
  672. }
  673.  
  674. settings = extend(settings, options);
  675.  
  676. // get player elements
  677. playBtn = player.querySelector('.ap__controls--toggle');
  678. playSvg = playBtn.querySelector('.icon-play');
  679. playSvgPath = playSvg.querySelector('path');
  680. prevBtn = player.querySelector('.ap__controls--prev');
  681. nextBtn = player.querySelector('.ap__controls--next');
  682. repeatBtn = player.querySelector('.ap__controls--repeat');
  683. volumeBtn = player.querySelector('.volume-btn');
  684. plBtn = player.querySelector('.ap__controls--playlist');
  685. curTime = player.querySelector('.track__time--current');
  686. durTime = player.querySelector('.track__time--duration');
  687. trackTitle = player.querySelector('.track__title');
  688. progressBar = player.querySelector('.progress__bar');
  689. preloadBar = player.querySelector('.progress__preload');
  690. volumeBar = player.querySelector('.volume__bar');
  691.  
  692. playList = settings.playList;
  693.  
  694. playBtn.addEventListener('click', playToggle, false);
  695. volumeBtn.addEventListener('click', volumeToggle, false);
  696. repeatBtn.addEventListener('click', repeatToggle, false);
  697.  
  698. progressBar.closest('.progress-container').addEventListener('mousedown', handlerBar, false);
  699. progressBar.closest('.progress-container').addEventListener('mousemove', seek, false);
  700.  
  701. document.documentElement.addEventListener('mouseup', seekingFalse, false);
  702.  
  703. volumeBar.closest('.volume').addEventListener('mousedown', handlerVol, false);
  704. volumeBar.closest('.volume').addEventListener('mousemove', setVolume);
  705. volumeBar.closest('.volume').addEventListener(wheel(), setVolume, false);
  706.  
  707. prevBtn.addEventListener('click', prev, false);
  708. nextBtn.addEventListener('click', next, false);
  709.  
  710. apActive = true;
  711.  
  712. // Create playlist
  713. renderPL();
  714. plBtn.addEventListener('click', plToggle, false);
  715.  
  716. // Create audio object
  717. audio = new Audio();
  718. audio.volume = settings.volume;
  719. audio.preload = 'auto';
  720. audio.id='audio'
  721.  
  722. audio.addEventListener('error', errorHandler, false);
  723. audio.addEventListener('timeupdate', timeUpdate, false);
  724. audio.addEventListener('ended', doEnd, false);
  725.  
  726. volumeBar.style.height = audio.volume * 100 + '%';
  727. volumeLength = volumeBar.css('height');
  728.  
  729. if(settings.confirmClose) {
  730. window.addEventListener("beforeunload", beforeUnload, false);
  731. }
  732.  
  733. if(isEmptyList()) {
  734. return false;
  735. }
  736. audio.src = playList[index].file;
  737. trackTitle.innerHTML = playList[index].title;
  738. document.body.append(audio)
  739.  
  740. if(settings.autoPlay) {
  741. audio.play();
  742. playBtn.classList.add('is-playing');
  743. playSvgPath.setAttribute('d', playSvg.getAttribute('data-pause'));
  744. plLi[index].classList.add('pl-list--current');
  745. notify(playList[index].title, {
  746. icon: playList[index].icon,
  747. body: 'Now playing'
  748. });
  749. }
  750. }
  751.  
  752. function changeDocumentTitle(title) {
  753. if(settings.changeDocTitle) {
  754. if(title) {
  755. document.title = title;
  756. }
  757. else {
  758. document.title = docTitle;
  759. }
  760. }
  761. }
  762.  
  763. function beforeUnload(evt) {
  764. if(!audio.paused) {
  765. var message = 'Music still playing';
  766. evt.returnValue = message;
  767. return message;
  768. }
  769. }
  770.  
  771. function errorHandler(evt) {
  772. if(isEmptyList()) {
  773. return;
  774. }
  775. var mediaError = {
  776. '1': 'MEDIA_ERR_ABORTED',
  777. '2': 'MEDIA_ERR_NETWORK',
  778. '3': 'MEDIA_ERR_DECODE',
  779. '4': 'MEDIA_ERR_SRC_NOT_SUPPORTED'
  780. };
  781. audio.pause();
  782. curTime.innerHTML = '--';
  783. durTime.innerHTML = '--';
  784. progressBar.style.width = 0;
  785. preloadBar.style.width = 0;
  786. playBtn.classList.remove('is-playing');
  787. playSvgPath.setAttribute('d', playSvg.getAttribute('data-play'));
  788. plLi[index] && plLi[index].classList.remove('pl-list--current');
  789. changeDocumentTitle();
  790. throw new Error('Houston we have a problem: ' + mediaError[evt.target.error.code]);
  791. }
  792.  
  793. /**
  794. * UPDATE PL
  795. */
  796. function updatePL(addList) {
  797. if(!apActive) {
  798. return 'Player is not yet initialized';
  799. }
  800. if(!Array.isArray(addList)) {
  801. return;
  802. }
  803. if(addList.length === 0) {
  804. return;
  805. }
  806.  
  807. var count = playList.length;
  808. var html = [];
  809. playList = addList
  810. audio.src = playList[index].file;
  811. trackTitle.innerHTML = playList[index].title;
  812. audio.currentTime = 0
  813. audio.pause()
  814. playToggle()
  815. }
  816.  
  817. /**
  818. * PlayList methods
  819. */
  820. function renderPL() {
  821. var html = [];
  822.  
  823. playList.forEach(function(item, i) {
  824. html.push(
  825. tplList.replace('{count}', i).replace('{title}', item.title)
  826. );
  827. });
  828.  
  829. pl = create('div', {
  830. 'className': 'pl-container',
  831. 'id': 'pl',
  832. 'innerHTML': '<ul class="pl-ul">' + (!isEmptyList() ? html.join('') : '<li class="pl-list--empty">PlayList is empty</li>') + '</ul>'
  833. });
  834.  
  835. player.parentNode.insertBefore(pl, player.nextSibling);
  836.  
  837. plUl = pl.querySelector('.pl-ul');
  838. plLi = plUl.querySelectorAll('li');
  839.  
  840. pl.addEventListener('click', listHandler, false);
  841. }
  842.  
  843. function listHandler(evt) {
  844. evt.preventDefault();
  845.  
  846. if(evt.target.matches('.pl-list__title') || evt.target.matches('.pl-list__track')) {
  847. var current = parseInt(evt.target.closest('.pl-list').getAttribute('data-track'), 10);
  848. if(index !== current) {
  849. index = current;
  850. play(current);
  851. }
  852. else {
  853. playToggle();
  854. }
  855. }
  856. else {
  857. if(!!evt.target.closest('.pl-list__remove')) {
  858. var parentEl = evt.target.closest('.pl-list');
  859. var isDel = parseInt(parentEl.getAttribute('data-track'), 10);
  860.  
  861. playList.splice(isDel, 1);
  862. parentEl.closest('.pl-ul').removeChild(parentEl);
  863.  
  864. plLi = pl.querySelectorAll('li');
  865.  
  866. [].forEach.call(plLi, function(el, i) {
  867. el.setAttribute('data-track', i);
  868. });
  869.  
  870. if(!audio.paused) {
  871.  
  872. if(isDel === index) {
  873. play(index);
  874. }
  875.  
  876. }
  877. else {
  878. if(isEmptyList()) {
  879. clearAll();
  880. }
  881. else {
  882. if(isDel === index) {
  883. if(isDel > playList.length - 1) {
  884. index -= 1;
  885. }
  886. audio.src = playList[index].file;
  887. trackTitle.innerHTML = playList[index].title;
  888. progressBar.style.width = 0;
  889. }
  890. }
  891. }
  892. if(isDel < index) {
  893. index--;
  894. }
  895. }
  896.  
  897. }
  898. }
  899.  
  900. function plActive() {
  901. if(audio.paused) {
  902. plLi[index].classList.remove('pl-list--current');
  903. return;
  904. }
  905. var current = index;
  906. for(var i = 0, len = plLi.length; len > i; i++) {
  907. plLi[i].classList.remove('pl-list--current');
  908. }
  909. plLi[current].classList.add('pl-list--current');
  910. }
  911.  
  912.  
  913. /**
  914. * Player methods
  915. */
  916. function play(currentIndex) {
  917.  
  918. if(isEmptyList()) {
  919. return clearAll();
  920. }
  921.  
  922. index = 0;
  923.  
  924. audio.src = playList[index].file;
  925. trackTitle.innerHTML = playList[index].title;
  926.  
  927. // Change document title
  928. changeDocumentTitle(playList[index].title);
  929.  
  930. // Audio play
  931. audio.play();
  932.  
  933. // Show notification
  934. notify(playList[index].title, {
  935. icon: playList[index].icon,
  936. body: 'Now playing',
  937. tag: 'music-player'
  938. });
  939.  
  940. // Toggle play button
  941. playBtn.classList.add('is-playing');
  942. playSvgPath.setAttribute('d', playSvg.getAttribute('data-pause'));
  943.  
  944. // Set active song playlist
  945. plActive();
  946. }
  947.  
  948. function prev() {
  949. play(index - 1);
  950. }
  951.  
  952. function next() {
  953. play(index + 1);
  954. }
  955.  
  956. function isEmptyList() {
  957. return playList.length === 0;
  958. }
  959.  
  960. function clearAll() {
  961. audio.pause();
  962. audio.src = '';
  963. trackTitle.innerHTML = 'queue is empty';
  964. curTime.innerHTML = '--';
  965. durTime.innerHTML = '--';
  966. progressBar.style.width = 0;
  967. preloadBar.style.width = 0;
  968. playBtn.classList.remove('is-playing');
  969. playSvgPath.setAttribute('d', playSvg.getAttribute('data-play'));
  970. if(!plUl.querySelector('.pl-list--empty')) {
  971. plUl.innerHTML = '<li class="pl-list--empty">PlayList is empty</li>';
  972. }
  973. changeDocumentTitle();
  974. }
  975.  
  976. function playToggle() {
  977. if(isEmptyList()) {
  978. return;
  979. }
  980. if(audio.paused) {
  981.  
  982. if(audio.currentTime === 0) {
  983. notify(playList[index].title, {
  984. icon: playList[index].icon,
  985. body: 'Now playing'
  986. });
  987. }
  988. changeDocumentTitle(playList[index].title);
  989.  
  990. audio.play();
  991.  
  992. playBtn.classList.add('is-playing');
  993. playSvgPath.setAttribute('d', playSvg.getAttribute('data-pause'));
  994. }
  995. else {
  996. changeDocumentTitle();
  997. audio.pause();
  998. playBtn.classList.remove('is-playing');
  999. playSvgPath.setAttribute('d', playSvg.getAttribute('data-play'));
  1000. }
  1001. plActive();
  1002. }
  1003.  
  1004. function volumeToggle() {
  1005. if(audio.muted) {
  1006. if(parseInt(volumeLength, 10) === 0) {
  1007. volumeBar.style.height = settings.volume * 100 + '%';
  1008. audio.volume = settings.volume;
  1009. }
  1010. else {
  1011. volumeBar.style.height = volumeLength;
  1012. }
  1013. audio.muted = false;
  1014. volumeBtn.classList.remove('has-muted');
  1015. }
  1016. else {
  1017. audio.muted = true;
  1018. volumeBar.style.height = 0;
  1019. volumeBtn.classList.add('has-muted');
  1020. }
  1021. }
  1022.  
  1023. function repeatToggle() {
  1024. if(repeatBtn.classList.contains('is-active')) {
  1025. repeating = false;
  1026. repeatBtn.classList.remove('is-active');
  1027. }
  1028. else {
  1029. repeating = true;
  1030. repeatBtn.classList.add('is-active');
  1031. }
  1032. }
  1033.  
  1034. function plToggle() {
  1035. plBtn.classList.toggle('is-active');
  1036. pl.classList.toggle('h-show');
  1037. }
  1038.  
  1039. function timeUpdate() {
  1040. if(audio.readyState === 0 || seeking) return;
  1041.  
  1042. var barlength = Math.round(audio.currentTime * (100 / audio.duration));
  1043. progressBar.style.width = barlength + '%';
  1044.  
  1045. var
  1046. curMins = Math.floor(audio.currentTime / 60),
  1047. curSecs = Math.floor(audio.currentTime - curMins * 60),
  1048. mins = Math.floor(audio.duration / 60),
  1049. secs = Math.floor(audio.duration - mins * 60);
  1050. (curSecs < 10) && (curSecs = '0' + curSecs);
  1051. (secs < 10) && (secs = '0' + secs);
  1052.  
  1053. curTime.innerHTML = curMins + ':' + curSecs;
  1054. durTime.innerHTML = mins + ':' + secs;
  1055.  
  1056. if(settings.buffered) {
  1057. var buffered = audio.buffered;
  1058. if(buffered.length) {
  1059. var loaded = Math.round(100 * buffered.end(0) / audio.duration);
  1060. preloadBar.style.width = loaded + '%';
  1061. }
  1062. }
  1063. }
  1064.  
  1065. /**
  1066. * TODO shuffle
  1067. */
  1068. function shuffle() {
  1069. if(shuffle) {
  1070. index = Math.round(Math.random() * playList.length);
  1071. }
  1072. }
  1073.  
  1074. function doEnd() {
  1075. if(index === playList.length - 1) {
  1076. if(!repeating) {
  1077. audio.pause();
  1078. plActive();
  1079. playBtn.classList.remove('is-playing');
  1080. playSvgPath.setAttribute('d', playSvg.getAttribute('data-play'));
  1081. return;
  1082. }
  1083. else {
  1084. play(0);
  1085. }
  1086. }
  1087. else {
  1088. play(index + 1);
  1089. }
  1090. }
  1091.  
  1092. function moveBar(evt, el, dir) {
  1093. var value;
  1094. if(dir === 'horizontal') {
  1095. value = Math.round( ((evt.clientX - el.offset().left) + window.pageXOffset) * 100 / el.parentNode.offsetWidth);
  1096. el.style.width = value + '%';
  1097. return value;
  1098. }
  1099. else {
  1100. if(evt.type === wheel()) {
  1101. value = parseInt(volumeLength, 10);
  1102. var delta = evt.deltaY || evt.detail || -evt.wheelDelta;
  1103. value = (delta > 0) ? value - 10 : value + 10;
  1104. }
  1105. else {
  1106. var offset = (el.offset().top + el.offsetHeight) - window.pageYOffset;
  1107. value = Math.round((offset - evt.clientY));
  1108. }
  1109. if(value > 100) value = wheelVolumeValue = 100;
  1110. if(value < 0) value = wheelVolumeValue = 0;
  1111. volumeBar.style.height = value + '%';
  1112. return value;
  1113. }
  1114. }
  1115.  
  1116. function handlerBar(evt) {
  1117. rightClick = (evt.which === 3) ? true : false;
  1118. seeking = true;
  1119. !rightClick && progressBar.classList.add('progress__bar--active');
  1120. seek(evt);
  1121. }
  1122.  
  1123. function handlerVol(evt) {
  1124. rightClick = (evt.which === 3) ? true : false;
  1125. seekingVol = true;
  1126. setVolume(evt);
  1127. }
  1128.  
  1129. function seek(evt) {
  1130. evt.preventDefault();
  1131. if(seeking && rightClick === false && audio.readyState !== 0) {
  1132. window.value = moveBar(evt, progressBar, 'horizontal');
  1133. }
  1134. }
  1135.  
  1136. function seekingFalse() {
  1137. if(seeking && rightClick === false && audio.readyState !== 0) {
  1138. audio.currentTime = audio.duration * (window.value / 100);
  1139. progressBar.classList.remove('progress__bar--active');
  1140. }
  1141. seeking = false;
  1142. seekingVol = false;
  1143. }
  1144.  
  1145. function setVolume(evt) {
  1146. evt.preventDefault();
  1147. volumeLength = volumeBar.css('height');
  1148. if(seekingVol && rightClick === false || evt.type === wheel()) {
  1149. var value = moveBar(evt, volumeBar.parentNode, 'vertical') / 100;
  1150. if(value <= 0) {
  1151. audio.volume = 0;
  1152. audio.muted = true;
  1153. volumeBtn.classList.add('has-muted');
  1154. }
  1155. else {
  1156. if(audio.muted) audio.muted = false;
  1157. audio.volume = value;
  1158. volumeBtn.classList.remove('has-muted');
  1159. }
  1160. }
  1161. }
  1162.  
  1163. function notify(title, attr) {
  1164. if(!settings.notification) {
  1165. return;
  1166. }
  1167. if(window.Notification === undefined) {
  1168. return;
  1169. }
  1170. attr.tag = 'AP music player';
  1171. }
  1172.  
  1173. /* Destroy method. Clear All */
  1174. function destroy() {
  1175. if(!apActive) return;
  1176.  
  1177. if(settings.confirmClose) {
  1178. window.removeEventListener('beforeunload', beforeUnload, false);
  1179. }
  1180.  
  1181. playBtn.removeEventListener('click', playToggle, false);
  1182. volumeBtn.removeEventListener('click', volumeToggle, false);
  1183. repeatBtn.removeEventListener('click', repeatToggle, false);
  1184. plBtn.removeEventListener('click', plToggle, false);
  1185.  
  1186. progressBar.closest('.progress-container').removeEventListener('mousedown', handlerBar, false);
  1187. progressBar.closest('.progress-container').removeEventListener('mousemove', seek, false);
  1188. document.documentElement.removeEventListener('mouseup', seekingFalse, false);
  1189.  
  1190. volumeBar.closest('.volume').removeEventListener('mousedown', handlerVol, false);
  1191. volumeBar.closest('.volume').removeEventListener('mousemove', setVolume);
  1192. volumeBar.closest('.volume').removeEventListener(wheel(), setVolume);
  1193. document.documentElement.removeEventListener('mouseup', seekingFalse, false);
  1194.  
  1195. prevBtn.removeEventListener('click', prev, false);
  1196. nextBtn.removeEventListener('click', next, false);
  1197.  
  1198. audio.removeEventListener('error', errorHandler, false);
  1199. audio.removeEventListener('timeupdate', timeUpdate, false);
  1200. audio.removeEventListener('ended', doEnd, false);
  1201.  
  1202. // Playlist
  1203. pl.removeEventListener('click', listHandler, false);
  1204. pl.parentNode.removeChild(pl);
  1205.  
  1206. audio.pause();
  1207. apActive = false;
  1208. index = 0;
  1209.  
  1210. playBtn.classList.remove('is-playing');
  1211. playSvgPath.setAttribute('d', playSvg.getAttribute('data-play'));
  1212. volumeBtn.classList.remove('has-muted');
  1213. plBtn.classList.remove('is-active');
  1214. repeatBtn.classList.remove('is-active');
  1215.  
  1216. // Remove player from the DOM if necessary
  1217. // player.parentNode.removeChild(player);
  1218. }
  1219.  
  1220.  
  1221. /**
  1222. * Helpers
  1223. */
  1224. function wheel() {
  1225. var wheel;
  1226. if ('onwheel' in document) {
  1227. wheel = 'wheel';
  1228. } else if ('onmousewheel' in document) {
  1229. wheel = 'mousewheel';
  1230. } else {
  1231. wheel = 'MozMousePixelScroll';
  1232. }
  1233. return wheel;
  1234. }
  1235.  
  1236. function extend(defaults, options) {
  1237. for(var name in options) {
  1238. if(defaults.hasOwnProperty(name)) {
  1239. defaults[name] = options[name];
  1240. }
  1241. }
  1242. return defaults;
  1243. }
  1244. function create(el, attr) {
  1245. var element = document.createElement(el);
  1246. if(attr) {
  1247. for(var name in attr) {
  1248. if(element[name] !== undefined) {
  1249. element[name] = attr[name];
  1250. }
  1251. }
  1252. }
  1253. return element;
  1254. }
  1255.  
  1256. Element.prototype.offset = function() {
  1257. var el = this.getBoundingClientRect(),
  1258. scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
  1259. scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  1260.  
  1261. return {
  1262. top: el.top + scrollTop,
  1263. left: el.left + scrollLeft
  1264. };
  1265. };
  1266.  
  1267. Element.prototype.css = function(attr) {
  1268. if(typeof attr === 'string') {
  1269. return getComputedStyle(this, '')[attr];
  1270. }
  1271. else if(typeof attr === 'object') {
  1272. for(var name in attr) {
  1273. if(this.style[name] !== undefined) {
  1274. this.style[name] = attr[name];
  1275. }
  1276. }
  1277. }
  1278. };
  1279.  
  1280. // matches polyfill
  1281. window.Element && function(ElementPrototype) {
  1282. ElementPrototype.matches = ElementPrototype.matches ||
  1283. ElementPrototype.matchesSelector ||
  1284. ElementPrototype.webkitMatchesSelector ||
  1285. ElementPrototype.msMatchesSelector ||
  1286. function(selector) {
  1287. var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
  1288. while (nodes[++i] && nodes[i] != node);
  1289. return !!nodes[i];
  1290. };
  1291. }(Element.prototype);
  1292.  
  1293. // closest polyfill
  1294. window.Element && function(ElementPrototype) {
  1295. ElementPrototype.closest = ElementPrototype.closest ||
  1296. function(selector) {
  1297. var el = this;
  1298. while (el.matches && !el.matches(selector)) el = el.parentNode;
  1299. return el.matches ? el : null;
  1300. };
  1301. }(Element.prototype);
  1302.  
  1303. /**
  1304. * Public methods
  1305. */
  1306. return {
  1307. init: init,
  1308. update: updatePL,
  1309. destroy: destroy,
  1310. playToggle:playToggle
  1311. };
  1312.  
  1313. })();
  1314.  
  1315. window.AP = AudioPlayer;
  1316.  
  1317. })(window);
  1318.  
  1319. // TEST: image for web notifications
  1320. var iconImage = 'http://funkyimg.com/i/21pX5.png';
  1321.  
  1322. AP.init({
  1323. playList: [
  1324. {'icon': iconImage, 'title': 'JWS-01', 'file': 'https://tnagano-web.s3.amazonaws.com/2021/JapaneseGenki3rdEdExerciseFiles_audios/Genki3rdEdAudio_JWS-01.mp3'}
  1325. ]
  1326. });
  1327.  
  1328. $(".container-fluid").click(function(event) {
  1329. var target = $(event.target);
  1330. if (target.is("a")) {
  1331. event.preventDefault();
  1332. let newAudio = [
  1333. {'icon': iconImage, 'title': target.text(), 'file':target.attr('href')}
  1334. ]
  1335. AP.update(newAudio)
  1336. }
  1337. });
  1338. $(document).on('keyup', function(e) {
  1339. e.preventDefault();
  1340. if (e.keyCode === 32) {
  1341. AP.playToggle()
  1342. }
  1343. });
  1344.  
  1345. })();