diff --git a/resources/APlayer.min.css b/resources/APlayer.min.css new file mode 100644 index 000000000..12b558391 --- /dev/null +++ b/resources/APlayer.min.css @@ -0,0 +1,3 @@ +.aplayer{background:#fff;font-family:Arial,Helvetica,sans-serif;margin:5px;box-shadow:0 2px 2px 0 rgba(0,0,0,.07),0 1px 5px 0 rgba(0,0,0,.1);border-radius:2px;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal;position:relative}.aplayer *{box-sizing:content-box}.aplayer svg{width:100%;height:100%}.aplayer svg circle,.aplayer svg path{fill:#fff}.aplayer.aplayer-withlist .aplayer-info{border-bottom:1px solid #e9e9e9}.aplayer.aplayer-withlist .aplayer-list{display:block}.aplayer.aplayer-withlist .aplayer-icon-order,.aplayer.aplayer-withlist .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-menu{display:inline}.aplayer.aplayer-withlrc .aplayer-pic{height:90px;width:90px}.aplayer.aplayer-withlrc .aplayer-info{margin-left:90px;height:90px;padding:10px 7px 0}.aplayer.aplayer-withlrc .aplayer-lrc{display:block}.aplayer.aplayer-narrow{width:66px}.aplayer.aplayer-narrow .aplayer-info,.aplayer.aplayer-narrow .aplayer-list{display:none}.aplayer.aplayer-narrow .aplayer-body,.aplayer.aplayer-narrow .aplayer-pic{height:66px;width:66px}.aplayer.aplayer-fixed{position:fixed;bottom:0;left:0;right:0;margin:0;z-index:99;overflow:visible;max-width:400px;box-shadow:none}.aplayer.aplayer-fixed .aplayer-list{margin-bottom:65px;border:1px solid #eee;border-bottom:none}.aplayer.aplayer-fixed .aplayer-body{position:fixed;bottom:0;left:0;right:0;margin:0;z-index:99;background:#fff;padding-right:18px;transition:all .3s ease;max-width:400px}.aplayer.aplayer-fixed .aplayer-lrc{display:block;position:fixed;bottom:10px;left:0;right:0;margin:0;z-index:98;pointer-events:none;text-shadow:-1px -1px 0 #fff}.aplayer.aplayer-fixed .aplayer-lrc:after,.aplayer.aplayer-fixed .aplayer-lrc:before{display:none}.aplayer.aplayer-fixed .aplayer-info{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:0 0;transform-origin:0 0;transition:all .3s ease;border-bottom:none;border-top:1px solid #e9e9e9}.aplayer.aplayer-fixed .aplayer-info .aplayer-music{width:calc(100% - 105px)}.aplayer.aplayer-fixed .aplayer-miniswitcher{display:block}.aplayer.aplayer-fixed.aplayer-narrow .aplayer-info{display:block;-webkit-transform:scaleX(0);transform:scaleX(0)}.aplayer.aplayer-fixed.aplayer-narrow .aplayer-body{width:66px!important}.aplayer.aplayer-fixed.aplayer-narrow .aplayer-miniswitcher .aplayer-icon{-webkit-transform:rotateY(0);transform:rotateY(0)}.aplayer.aplayer-fixed .aplayer-icon-back,.aplayer.aplayer-fixed .aplayer-icon-forward,.aplayer.aplayer-fixed .aplayer-icon-lrc,.aplayer.aplayer-fixed .aplayer-icon-play{display:inline-block}.aplayer.aplayer-fixed .aplayer-icon-back,.aplayer.aplayer-fixed .aplayer-icon-forward,.aplayer.aplayer-fixed .aplayer-icon-menu,.aplayer.aplayer-fixed .aplayer-icon-play{position:absolute;bottom:27px;width:20px;height:20px}.aplayer.aplayer-fixed .aplayer-icon-back{right:75px}.aplayer.aplayer-fixed .aplayer-icon-play{right:50px}.aplayer.aplayer-fixed .aplayer-icon-forward{right:25px}.aplayer.aplayer-fixed .aplayer-icon-menu{right:0}.aplayer.aplayer-arrow .aplayer-icon-loop,.aplayer.aplayer-arrow .aplayer-icon-order,.aplayer.aplayer-mobile .aplayer-icon-volume-down{display:none}.aplayer.aplayer-loading .aplayer-info .aplayer-controller .aplayer-loading-icon{display:block}.aplayer.aplayer-loading .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb{-webkit-transform:scale(1);transform:scale(1)}.aplayer .aplayer-body{position:relative}.aplayer .aplayer-icon{width:15px;height:15px;border:none;background-color:transparent;outline:none;cursor:pointer;opacity:.8;vertical-align:middle;padding:0;font-size:12px;margin:0;display:inline-block}.aplayer .aplayer-icon path{transition:all .2s ease-in-out}.aplayer .aplayer-icon-back,.aplayer .aplayer-icon-forward,.aplayer .aplayer-icon-lrc,.aplayer .aplayer-icon-order,.aplayer .aplayer-icon-play{display:none}.aplayer .aplayer-icon-lrc-inactivity svg{opacity:.4}.aplayer .aplayer-icon-forward{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.aplayer .aplayer-lrc-content{display:none}.aplayer .aplayer-pic{position:relative;float:left;height:66px;width:66px;background-size:cover;background-position:50%;transition:all .3s ease;cursor:pointer}.aplayer .aplayer-pic:hover .aplayer-button{opacity:1}.aplayer .aplayer-pic .aplayer-button{position:absolute;border-radius:50%;opacity:.8;text-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2);background:rgba(0,0,0,.2);transition:all .1s ease}.aplayer .aplayer-pic .aplayer-button path{fill:#fff}.aplayer .aplayer-pic .aplayer-hide{display:none}.aplayer .aplayer-pic .aplayer-play{width:26px;height:26px;border:2px solid #fff;bottom:50%;right:50%;margin:0 -15px -15px 0}.aplayer .aplayer-pic .aplayer-play svg{position:absolute;top:3px;left:4px;height:20px;width:20px}.aplayer .aplayer-pic .aplayer-pause{width:16px;height:16px;border:2px solid #fff;bottom:4px;right:4px}.aplayer .aplayer-pic .aplayer-pause svg{position:absolute;top:2px;left:2px;height:12px;width:12px}.aplayer .aplayer-info{margin-left:66px;padding:14px 7px 0 10px;height:66px;box-sizing:border-box}.aplayer .aplayer-info .aplayer-music{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin:0 0 13px 5px;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;cursor:default;padding-bottom:2px;height:20px}.aplayer .aplayer-info .aplayer-music .aplayer-title{font-size:14px}.aplayer .aplayer-info .aplayer-music .aplayer-author{font-size:12px;color:#666}.aplayer .aplayer-info .aplayer-controller{position:relative;display:flex}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap{margin:0 0 0 5px;padding:4px 0;cursor:pointer!important;flex:1}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap:hover .aplayer-bar .aplayer-played .aplayer-thumb{-webkit-transform:scale(1);transform:scale(1)}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar{position:relative;height:2px;width:100%;background:#cdcdcd}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-loaded{position:absolute;left:0;top:0;bottom:0;background:#aaa;height:2px;transition:all .5s ease}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played{position:absolute;left:0;top:0;bottom:0;height:2px}.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb{position:absolute;top:0;right:5px;margin-top:-4px;margin-right:-10px;height:10px;width:10px;border-radius:50%;cursor:pointer;transition:all .3s ease-in-out;-webkit-transform:scale(0);transform:scale(0)}.aplayer .aplayer-info .aplayer-controller .aplayer-time{position:relative;right:0;bottom:4px;height:17px;color:#999;font-size:11px;padding-left:7px}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-time-inner{vertical-align:middle}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon{cursor:pointer;transition:all .2s ease}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon path{fill:#666}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-loop{margin-right:2px}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon:hover path{fill:#000}.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-menu,.aplayer .aplayer-info .aplayer-controller .aplayer-time.aplayer-time-narrow .aplayer-icon-menu,.aplayer .aplayer-info .aplayer-controller .aplayer-time.aplayer-time-narrow .aplayer-icon-mode{display:none}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap{position:relative;display:inline-block;margin-left:3px;cursor:pointer!important}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap:hover .aplayer-volume-bar-wrap{height:40px}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap{position:absolute;bottom:15px;right:-3px;width:25px;height:0;z-index:99;overflow:hidden;transition:all .2s ease-in-out}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap.aplayer-volume-bar-wrap-active{height:40px}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap .aplayer-volume-bar{position:absolute;bottom:0;right:10px;width:5px;height:35px;background:#aaa;border-radius:2.5px;overflow:hidden}.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap .aplayer-volume-bar .aplayer-volume{position:absolute;bottom:0;right:0;width:5px;transition:all .1s ease}.aplayer .aplayer-info .aplayer-controller .aplayer-loading-icon{display:none}.aplayer .aplayer-info .aplayer-controller .aplayer-loading-icon svg{position:absolute;-webkit-animation:rotate 1s linear infinite;animation:rotate 1s linear infinite}.aplayer .aplayer-lrc{display:none;position:relative;height:30px;text-align:center;overflow:hidden;margin:-10px 0 7px}.aplayer .aplayer-lrc:before{top:0;height:10%;background:linear-gradient(180deg,#fff 0,hsla(0,0%,100%,0));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#00ffffff",GradientType=0)}.aplayer .aplayer-lrc:after,.aplayer .aplayer-lrc:before{position:absolute;z-index:1;display:block;overflow:hidden;width:100%;content:" "}.aplayer .aplayer-lrc:after{bottom:0;height:33%;background:linear-gradient(180deg,hsla(0,0%,100%,0) 0,hsla(0,0%,100%,.8));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#00ffffff",endColorstr="#ccffffff",GradientType=0)}.aplayer .aplayer-lrc p{font-size:12px;color:#666;line-height:16px!important;height:16px!important;padding:0!important;margin:0!important;transition:all .5s ease-out;opacity:.4;overflow:hidden}.aplayer .aplayer-lrc p.aplayer-lrc-current{opacity:1;overflow:visible;height:auto!important;min-height:16px}.aplayer .aplayer-lrc.aplayer-lrc-hide{display:none}.aplayer .aplayer-lrc .aplayer-lrc-contents{width:100%;transition:all .5s ease-out;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;cursor:default}.aplayer .aplayer-list{overflow:auto;transition:all .5s ease;will-change:height;display:none;overflow:hidden}.aplayer .aplayer-list.aplayer-list-hide{max-height:0!important}.aplayer .aplayer-list ol{list-style-type:none;margin:0;padding:0;overflow-y:auto}.aplayer .aplayer-list ol::-webkit-scrollbar{width:5px}.aplayer .aplayer-list ol::-webkit-scrollbar-thumb{border-radius:3px;background-color:#eee}.aplayer .aplayer-list ol::-webkit-scrollbar-thumb:hover{background-color:#ccc}.aplayer .aplayer-list ol li{position:relative;height:32px;line-height:32px;padding:0 15px;font-size:12px;border-top:1px solid #e9e9e9;cursor:pointer;transition:all .2s ease;overflow:hidden;margin:0}.aplayer .aplayer-list ol li:first-child{border-top:none}.aplayer .aplayer-list ol li:hover{background:#efefef}.aplayer .aplayer-list ol li.aplayer-list-light{background:#e9e9e9}.aplayer .aplayer-list ol li.aplayer-list-light .aplayer-list-cur{display:inline-block}.aplayer .aplayer-list ol li .aplayer-list-cur{display:none;width:3px;height:22px;position:absolute;left:0;top:5px;cursor:pointer}.aplayer .aplayer-list ol li .aplayer-list-index{color:#666;margin-right:12px;cursor:pointer}.aplayer .aplayer-list ol li .aplayer-list-author{color:#666;float:right;cursor:pointer}.aplayer .aplayer-notice{opacity:0;position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);font-size:12px;border-radius:4px;padding:5px 10px;transition:all .3s ease-in-out;overflow:hidden;color:#fff;pointer-events:none;background-color:#f4f4f5;color:#909399}.aplayer .aplayer-miniswitcher{display:none;position:absolute;top:0;right:0;bottom:0;height:100%;background:#e6e6e6;width:18px;border-radius:0 2px 2px 0}.aplayer .aplayer-miniswitcher .aplayer-icon{height:100%;width:100%;-webkit-transform:rotateY(180deg);transform:rotateY(180deg);transition:all .3s ease}.aplayer .aplayer-miniswitcher .aplayer-icon path{fill:#666}.aplayer .aplayer-miniswitcher .aplayer-icon:hover path{fill:#000}@-webkit-keyframes aplayer-roll{0%{left:0}to{left:-100%}}@keyframes aplayer-roll{0%{left:0}to{left:-100%}}@-webkit-keyframes rotate{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotate{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}} + +/*# sourceMappingURL=APlayer.min.css.map*/ \ No newline at end of file diff --git a/resources/APlayer.min.js b/resources/APlayer.min.js new file mode 100644 index 000000000..6ba17e35b --- /dev/null +++ b/resources/APlayer.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("APlayer",[],t):"object"==typeof exports?exports.APlayer=t():e.APlayer=t()}(window,function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var a=t[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:i})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=41)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=/mobile/i.test(window.navigator.userAgent),a={secondToTime:function(e){var t=Math.floor(e/3600),n=Math.floor((e-3600*t)/60),i=Math.floor(e-3600*t-60*n);return(t>0?[t,n,i]:[n,i]).map(function(e){return e<10?"0"+e:""+e}).join(":")},getElementViewLeft:function(e){var t=e.offsetLeft,n=e.offsetParent,i=document.body.scrollLeft+document.documentElement.scrollLeft;if(document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement)for(;null!==n&&n!==e;)t+=n.offsetLeft,n=n.offsetParent;else for(;null!==n;)t+=n.offsetLeft,n=n.offsetParent;return t-i},getElementViewTop:function(e,t){for(var n,i=e.offsetTop,a=e.offsetParent;null!==a;)i+=a.offsetTop,a=a.offsetParent;return n=document.body.scrollTop+document.documentElement.scrollTop,t?i:i-n},isMobile:i,storage:{set:function(e,t){localStorage.setItem(e,t)},get:function(e){return localStorage.getItem(e)}},nameMap:{dragStart:i?"touchstart":"mousedown",dragMove:i?"touchmove":"mousemove",dragEnd:i?"touchend":"mouseup"},randomOrder:function(e){return function(e){for(var t=e.length-1;t>=0;t--){var n=Math.floor(Math.random()*(t+1)),i=e[n];e[n]=e[t],e[t]=i}return e}([].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t\n ',t+=r(n+s),t+='\n ',t+=r(e.name),t+='\n ',t+=r(e.artist),t+="\n\n"}),t}},function(e,t,n){"use strict";e.exports=n(15)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=g(n(33)),a=g(n(32)),r=g(n(31)),o=g(n(30)),s=g(n(29)),l=g(n(28)),u=g(n(27)),c=g(n(26)),p=g(n(25)),d=g(n(24)),h=g(n(23)),y=g(n(22)),f=g(n(21)),v=g(n(20)),m=g(n(19));function g(e){return e&&e.__esModule?e:{default:e}}var w={play:i.default,pause:a.default,volumeUp:r.default,volumeDown:o.default,volumeOff:s.default,orderRandom:l.default,orderList:u.default,menu:c.default,loopAll:p.default,loopOne:d.default,loopNone:h.default,loading:y.default,right:f.default,skip:v.default,lrc:m.default};t.default=w},function(e,t,n){"use strict";var i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};i=function(){return this}();try{i=i||Function("return this")()||(0,eval)("this")}catch(e){"object"===("undefined"==typeof window?"undefined":a(window))&&(i=window)}e.exports=i},function(e,t,n){"use strict";var i,a,r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};void 0===(a="function"==typeof(i=function(){if("object"===("undefined"==typeof window?"undefined":r(window))&&void 0!==document.querySelectorAll&&void 0!==window.pageYOffset&&void 0!==history.pushState){var e=function(e,t,n,i){return n>i?t:e+(t-e)*((a=n/i)<.5?4*a*a*a:(a-1)*(2*a-2)*(2*a-2)+1);var a},t=function(t,n,i,a){n=n||500;var r=(a=a||window).scrollTop||window.pageYOffset;if("number"==typeof t)var o=parseInt(t);else var o=function(e,t){return"HTML"===e.nodeName?-t:e.getBoundingClientRect().top+t}(t,r);var s=Date.now(),l=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||function(e){window.setTimeout(e,15)};!function u(){var c=Date.now()-s;a!==window?a.scrollTop=e(r,o,c,n):window.scroll(0,e(r,o,c,n)),c>n?"function"==typeof i&&i(t):l(u)}()},n=function(e){if(!e.defaultPrevented){e.preventDefault(),location.hash!==this.hash&&window.history.pushState(null,null,this.hash);var n=document.getElementById(this.hash.substring(1));if(!n)return;t(n,500,function(e){location.replace("#"+e.id)})}};return document.addEventListener("DOMContentLoaded",function(){for(var e,t=document.querySelectorAll('a[href^="#"]:not([href="#"])'),i=t.length;e=t[--i];)e.addEventListener("click",n,!1)}),t}})?i.call(t,n,t,e):i)||(e.exports=a)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n1),n=0===this.audios.length;this.player.template.listOl.innerHTML+=(0,a.default)({theme:this.player.options.theme,audio:e,index:this.audios.length+1}),this.audios=this.audios.concat(e),t&&this.audios.length>1&&this.player.container.classList.add("aplayer-withlist"),this.player.randomOrder=r.default.randomOrder(this.audios.length),this.player.template.listCurs=this.player.container.querySelectorAll(".aplayer-list-cur"),this.player.template.listCurs[this.audios.length-1].style.backgroundColor=e.theme||this.player.options.theme,n&&("random"===this.player.options.order?this.switch(this.player.randomOrder[0]):this.switch(0))}},{key:"remove",value:function(e){if(this.player.events.trigger("listremove",{index:e}),this.audios[e])if(this.audios.length>1){var t=this.player.container.querySelectorAll(".aplayer-list li");t[e].remove(),this.audios.splice(e,1),this.player.lrc&&this.player.lrc.remove(e),e===this.index&&(this.audios[e]?this.switch(e):this.switch(e-1)),this.index>e&&this.index--;for(var n=e;nt&&!e.player.audio.paused&&(e.player.container.classList.remove("aplayer-loading"),i=!1),t=n)},100)}},{key:"enable",value:function(e){this["enable"+e+"Checker"]=!0,"fps"===e&&this.initfpsChecker()}},{key:"disable",value:function(e){this["enable"+e+"Checker"]=!1}},{key:"destroy",value:function(){var e=this;this.types.forEach(function(t){e["enable"+t+"Checker"]=!1,e[t+"Checker"]&&clearInterval(e[t+"Checker"])})}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n1?"one"===e.player.options.loop?(e.player.options.loop="none",e.player.template.loop.innerHTML=r.default.loopNone):"none"===e.player.options.loop?(e.player.options.loop="all",e.player.template.loop.innerHTML=r.default.loopAll):"all"===e.player.options.loop&&(e.player.options.loop="one",e.player.template.loop.innerHTML=r.default.loopOne):"one"===e.player.options.loop||"all"===e.player.options.loop?(e.player.options.loop="none",e.player.template.loop.innerHTML=r.default.loopNone):"none"===e.player.options.loop&&(e.player.options.loop="all",e.player.template.loop.innerHTML=r.default.loopAll)})}},{key:"initMenuButton",value:function(){var e=this;this.player.template.menu.addEventListener("click",function(){e.player.list.toggle()})}},{key:"initMiniSwitcher",value:function(){var e=this;this.player.template.miniSwitcher.addEventListener("click",function(){e.player.setMode("mini"===e.player.mode?"normal":"mini")})}},{key:"initSkipButton",value:function(){var e=this;this.player.template.skipBackButton.addEventListener("click",function(){e.player.skipBack()}),this.player.template.skipForwardButton.addEventListener("click",function(){e.player.skipForward()}),this.player.template.skipPlayButton.addEventListener("click",function(){e.player.toggle()})}},{key:"initLrcButton",value:function(){var e=this;this.player.template.lrcButton.addEventListener("click",function(){e.player.template.lrcButton.classList.contains("aplayer-icon-lrc-inactivity")?(e.player.template.lrcButton.classList.remove("aplayer-icon-lrc-inactivity"),e.player.lrc&&e.player.lrc.show()):(e.player.template.lrcButton.classList.add("aplayer-icon-lrc-inactivity"),e.player.lrc&&e.player.lrc.hide())})}}]),e}();t.default=s},function(e,t,n){var i=n(2);e.exports=function(e){"use strict";e=e||{};var t="",n=i.$each,a=e.lyrics,r=(e.$value,e.$index,i.$escape);return n(a,function(e,n){t+="\n \n"}),t}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i,a=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:this.player.audio.currentTime;if(this.index>this.current.length-1||e=this.current[this.index+1][0])for(var t=0;t=this.current[t][0]&&(!this.current[t+1]||e=200&&n.status<300||304===n.status?t.parsed[e]=t.parse(n.responseText):(t.player.notice("LRC file request fails: status "+n.status),t.parsed[e]=[["00:00","Not available"]]),t.container.innerHTML=(0,o.default)({lyrics:t.parsed[e]}),t.update(0),t.current=t.parsed[e])};var i=this.player.list.audios[e].lrc;n.open("get",i,!0),n.send(null)}else this.player.list.audios[e].lrc?this.parsed[e]=this.parse(this.player.list.audios[e].lrc):this.parsed[e]=[["00:00","Not available"]];this.container.innerHTML=(0,o.default)({lyrics:this.parsed[e]}),this.update(0),this.current=this.parsed[e]}},{key:"parse",value:function(e){if(e){for(var t=(e=e.replace(/([^\]^\n])\[/g,function(e,t){return t+"\n["})).split("\n"),n=[],i=t.length,a=0;a/g,"").replace(/^\s+|\s+$/g,"");if(r)for(var s=r.length,l=0;l]/;a.$escape=function(e){return function(e){var t=""+e,n=r.exec(t);if(!n)return e;var i="",a=void 0,o=void 0,s=void 0;for(a=n.index,o=0;a\n \n
',t+=s.play,t+='
\n \n \n
\n
\n\n
\n
\n
\n'):(t+='\n
\n
\n
',t+=s.play,t+='
\n
\n
\n
\n No audio\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n ',t+=s.loading,t+='\n \n
\n
\n
\n
\n \n 00:00 / 00:00\n \n \n ',t+=s.skip,t+='\n \n \n ',t+=s.play,t+='\n \n \n ',t+=s.skip,t+='\n \n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n \n
\n
\n
\n
\n
\n
\n
\n '},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t){e.exports=''},function(e,t,n){"use strict";var i,a,r=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function l(e){if(i===setTimeout)return setTimeout(e,0);if((i===o||!i)&&setTimeout)return i=setTimeout,setTimeout(e,0);try{return i(e,0)}catch(t){try{return i.call(null,e,0)}catch(t){return i.call(this,e,0)}}}!function(){try{i="function"==typeof setTimeout?setTimeout:o}catch(e){i=o}try{a="function"==typeof clearTimeout?clearTimeout:s}catch(e){a=s}}();var u,c=[],p=!1,d=-1;function h(){p&&u&&(p=!1,u.length?c=u.concat(c):d=-1,c.length&&y())}function y(){if(!p){var e=l(h);p=!0;for(var t=c.length;t;){for(u=c,c=[];++d1)for(var n=1;n=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(35),t.setImmediate=setImmediate,t.clearImmediate=clearImmediate},function(e,t,n){"use strict";(function(t){var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i=setTimeout;function a(){}function r(e){if(!(this instanceof r))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],c(e,this)}function o(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,r._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var i;try{i=n(e._value)}catch(e){return void l(t.promise,e)}s(t.promise,i)}else(1===e._state?s:l)(t.promise,e._value)})):e._deferreds.push(t)}function s(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"===(void 0===t?"undefined":n(t))||"function"==typeof t)){var i=t.then;if(t instanceof r)return e._state=3,e._value=t,void u(e);if("function"==typeof i)return void c((a=i,o=t,function(){a.apply(o,arguments)}),e)}e._state=1,e._value=t,u(e)}catch(t){l(e,t)}var a,o}function l(e,t){e._state=2,e._value=t,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&r._immediateFn(function(){e._handled||r._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;t1&&this.container.classList.add("aplayer-withlist"),r.default.isMobile&&this.container.classList.add("aplayer-mobile"),this.arrow=this.container.offsetWidth<=300,this.arrow&&this.container.classList.add("aplayer-arrow"),this.container=this.options.container,2===this.options.lrcType||!0===this.options.lrcType)for(var n=this.container.getElementsByClassName("aplayer-lrc-content"),i=0;i1?(e.notice("An audio error has occurred, player will skip forward in 2 seconds."),t=setTimeout(function(){e.skipForward(),e.paused||e.play()},2e3)):1===e.list.audios.length&&e.notice("An audio error has occurred.")}),this.events.on("listswitch",function(){t&&clearTimeout(t)}),this.on("ended",function(){"none"===e.options.loop?"list"===e.options.order?e.list.index0&&void 0!==arguments[0]?arguments[0]:this.list.audios[this.list.index].theme||this.options.theme,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.list.index;(!(arguments.length>2&&void 0!==arguments[2])||arguments[2])&&this.list.audios[t]&&(this.list.audios[t].theme=e),this.template.listCurs[t]&&(this.template.listCurs[t].style.backgroundColor=e),t===this.list.index&&(this.template.pic.style.backgroundColor=e,this.template.played.style.background=e,this.template.thumb.style.background=e,this.template.volume.style.background=e)}},{key:"seek",value:function(e){e=Math.max(e,0),e=Math.min(e,this.duration),this.audio.currentTime=e,this.bar.set("played",e/this.duration,"width"),this.template.ptime.innerHTML=r.default.secondToTime(e)}},{key:"setUIPlaying",value:function(){var e=this;if(this.paused&&(this.paused=!1,this.template.button.classList.remove("aplayer-play"),this.template.button.classList.add("aplayer-pause"),this.template.button.innerHTML="",setTimeout(function(){e.template.button.innerHTML=o.default.pause},100),this.template.skipPlayButton.innerHTML=o.default.pause),this.timer.enable("loading"),this.options.mutex)for(var t=0;t=.95?this.template.volumeButton.innerHTML=o.default.volumeUp:this.volume()>0?this.template.volumeButton.innerHTML=o.default.volumeDown:this.template.volumeButton.innerHTML=o.default.volumeOff}},{key:"volume",value:function(e,t){return e=parseFloat(e),isNaN(e)||(e=Math.max(e,0),e=Math.min(e,1),this.bar.set("volume",e,"height"),t||this.storage.set("volume",e),this.audio.volume=e,this.audio.muted&&(this.audio.muted=!1),this.switchVolumeIcon()),this.audio.muted?0:this.audio.volume}},{key:"on",value:function(e,t){this.events.on(e,t)}},{key:"toggle",value:function(){this.template.button.classList.contains("aplayer-play")?this.play():this.template.button.classList.contains("aplayer-pause")&&this.pause()}},{key:"switchAudio",value:function(e){this.list.switch(e)}},{key:"addAudio",value:function(e){this.list.add(e)}},{key:"removeAudio",value:function(e){this.list.remove(e)}},{key:"destroy",value:function(){m.splice(m.indexOf(this),1),this.pause(),this.container.innerHTML="",this.audio.src="",this.timer.destroy(),this.events.trigger("destroy")}},{key:"setMode",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"normal";this.mode=e,"mini"===e?this.container.classList.add("aplayer-narrow"):"normal"===e&&this.container.classList.remove("aplayer-narrow")}},{key:"notice",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2e3,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:.8;this.template.notice.innerHTML=e,this.template.notice.style.opacity=i,this.noticeTime&&clearTimeout(this.noticeTime),this.events.trigger("noticeshow",{text:e}),n&&(this.noticeTime=setTimeout(function(){t.template.notice.style.opacity=0,t.events.trigger("noticehide")},n))}},{key:"prevIndex",value:function(){if(!(this.list.audios.length>1))return 0;if("list"===this.options.order)return this.list.index-1<0?this.list.audios.length-1:this.list.index-1;if("random"===this.options.order){var e=this.randomOrder.indexOf(this.list.index);return 0===e?this.randomOrder[this.randomOrder.length-1]:this.randomOrder[e-1]}}},{key:"nextIndex",value:function(){if(!(this.list.audios.length>1))return 0;if("list"===this.options.order)return(this.list.index+1)%this.list.audios.length;if("random"===this.options.order){var e=this.randomOrder.indexOf(this.list.index);return e===this.randomOrder.length-1?this.randomOrder[0]:this.randomOrder[e+1]}}},{key:"skipBack",value:function(){this.list.switch(this.prevIndex())}},{key:"skipForward",value:function(){this.list.switch(this.nextIndex())}},{key:"duration",get:function(){return isNaN(this.audio.duration)?0:this.audio.duration}}],[{key:"version",get:function(){return"1.10.1"}}]),e}();t.default=g},,function(e,t,n){},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(40);var i,a=n(38),r=(i=a)&&i.__esModule?i:{default:i};console.log("\n %c APlayer v1.10.1 af84efb %c http://aplayer.js.org \n","color: #fadfa3; background: #030307; padding:5px 0;","background: #fadfa3; padding:5px 0;"),t.default=r.default}]).default}); +//# sourceMappingURL=APlayer.min.js.map \ No newline at end of file diff --git a/resources/aplayer-share.js b/resources/aplayer-share.js new file mode 100644 index 000000000..6df55fb71 --- /dev/null +++ b/resources/aplayer-share.js @@ -0,0 +1,136 @@ +/** + * APlayer integration for Navidrome Share Links + * Works with public share links without authentication + */ +(function() { + 'use strict'; + + // Wait for DOM and APlayer to be ready + function initAPlayer() { + console.log('APlayer initialization started'); + + // Check if APlayer is loaded + if (typeof APlayer === 'undefined') { + console.error('APlayer library not loaded - checking if script loaded'); + + // Try to load APlayer if not available + const aplayerScript = document.querySelector('script[src*="APlayer.min.js"]'); + if (!aplayerScript) { + console.error('APlayer script tag not found in DOM'); + } else { + console.log('APlayer script tag found:', aplayerScript.src); + } + return; + } + console.log('APlayer library loaded'); + + // Get share info from the page (injected by server) + const shareInfoElement = document.getElementById('share-info'); + if (!shareInfoElement) { + console.error('Share info not found'); + return; + } + console.log('Share info element found:', shareInfoElement.textContent); + + let shareInfo; + try { + shareInfo = JSON.parse(shareInfoElement.textContent); + } catch (e) { + console.error('Failed to parse share info:', e); + return; + } + + if (!shareInfo || !shareInfo.tracks || shareInfo.tracks.length === 0) { + console.error('No tracks found in share'); + return; + } + + // Get base URL from the page + const baseURL = window.NavidromeConfig?.baseURL || ''; + + // Convert share tracks to APlayer format + const playlist = shareInfo.tracks.map(function(track) { + // Stream URL uses the encoded track ID (contains JWT token) + const streamUrl = baseURL + '/share/s/' + track.id; + + // Cover art URL - we'll construct it from the share's image + const coverUrl = shareInfo.imageUrl || baseURL + '/android-chrome-192x192.png'; + + return { + name: track.title || 'Unknown Title', + artist: track.artist || 'Unknown Artist', + url: streamUrl, + cover: coverUrl, + theme: '#b7daff' + }; + }); + + // Initialize APlayer + const container = document.getElementById('aplayer'); + if (!container) { + console.error('APlayer container not found'); + return; + } + console.log('APlayer container found:', container); + console.log('Container dimensions:', container.offsetWidth, 'x', container.offsetHeight); + console.log('Container styles:', window.getComputedStyle(container)); + + console.log('Creating APlayer with playlist:', playlist); + + let ap; + try { + ap = new APlayer({ + container: container, + lrcType: 0, + audio: playlist, + autoplay: false, + theme: '#b7daff', + loop: 'all', + order: 'list', + preload: 'auto', + volume: 0.7, + mutex: true, + listFolded: false, + listMaxHeight: 90, + fixed: false, + mini: false, + }); + + // Log initialization + console.log('APlayer initialized with', playlist.length, 'tracks'); + console.log('APlayer instance:', ap); + + // Check if APlayer created DOM elements + setTimeout(() => { + const aplayerElements = container.querySelectorAll('*'); + console.log('APlayer created', aplayerElements.length, 'child elements'); + + if (aplayerElements.length === 0) { + console.error('APlayer did not create any child elements - initialization failed'); + } else { + console.log('APlayer child elements:', aplayerElements); + } + }, 100); + + } catch (error) { + console.error('APlayer initialization failed:', error); + return; + } + + // Optional: Add event listeners + ap.on('play', function() { + console.log('Playing:', ap.list.audios[ap.list.index].name); + }); + + ap.on('error', function() { + console.error('Playback error'); + }); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initAPlayer); + } else { + initAPlayer(); + } +})(); diff --git a/resources/aplayer.html b/resources/aplayer.html new file mode 100644 index 000000000..69afaa08f --- /dev/null +++ b/resources/aplayer.html @@ -0,0 +1,148 @@ + + + + + + {{.ShareDescription}} - Navidrome + + + + + + + + +
+
+

{{.ShareDescription}}

+

Shared Music Player

+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + diff --git a/server/public/handle_aplayer_assets.go b/server/public/handle_aplayer_assets.go new file mode 100644 index 000000000..2fe83b27a --- /dev/null +++ b/server/public/handle_aplayer_assets.go @@ -0,0 +1,53 @@ +package public + +import ( + "io" + "net/http" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/resources" +) + +// handleAPlayerCSS serves the vendored APlayer CSS file +func (pub *Router) handleAPlayerCSS(w http.ResponseWriter, r *http.Request) { + cssFile, err := resources.FS().Open("APlayer.min.css") + if err != nil { + log.Error(r.Context(), "Could not find APlayer.min.css", err) + http.Error(w, "CSS file not found", http.StatusNotFound) + return + } + defer cssFile.Close() + + cssContent, err := io.ReadAll(cssFile) + if err != nil { + log.Error(r.Context(), "Error reading APlayer.min.css", err) + http.Error(w, "Error reading CSS file", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/css; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year + _, _ = w.Write(cssContent) +} + +// handleAPlayerJS serves the vendored APlayer JavaScript file +func (pub *Router) handleAPlayerJS(w http.ResponseWriter, r *http.Request) { + jsFile, err := resources.FS().Open("APlayer.min.js") + if err != nil { + log.Error(r.Context(), "Could not find APlayer.min.js", err) + http.Error(w, "JS file not found", http.StatusNotFound) + return + } + defer jsFile.Close() + + jsContent, err := io.ReadAll(jsFile) + if err != nil { + log.Error(r.Context(), "Error reading APlayer.min.js", err) + http.Error(w, "Error reading JS file", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year + _, _ = w.Write(jsContent) +} diff --git a/server/public/handle_shares.go b/server/public/handle_shares.go index 15e63d4db..7cfacc209 100644 --- a/server/public/handle_shares.go +++ b/server/public/handle_shares.go @@ -1,20 +1,29 @@ package public import ( + "bytes" "context" + "encoding/json" "errors" + "html/template" + "io" "net/http" "path" + "time" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/publicurl" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/resources" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/ui" . "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/req" + "github.com/navidrome/navidrome/utils/slice" + "github.com/navidrome/navidrome/utils/str" ) func (pub *Router) handleShares(w http.ResponseWriter, r *http.Request) { @@ -62,6 +71,139 @@ func (pub *Router) handleM3U(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(s.ToM3U8())) //nolint:gosec } +func (pub *Router) handleAPlayer(w http.ResponseWriter, r *http.Request) { + id, err := req.Params(r).String(":id") + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Load share + s, err := pub.share.Load(r.Context(), id) + if err != nil { + checkShareError(r.Context(), w, err, id) + return + } + + // Map share info for APlayer + s = pub.mapShareInfo(r, *s) + + // Read template + tmplData, err := resources.FS().Open("aplayer.html") + if err != nil { + log.Error(r.Context(), "Could not find aplayer.html template", err) + http.Error(w, "Template not found", http.StatusInternalServerError) + return + } + defer tmplData.Close() + + tmplContent, err := io.ReadAll(tmplData) + if err != nil { + log.Error(r.Context(), "Error reading aplayer.html template", err) + http.Error(w, "Error reading template", http.StatusInternalServerError) + return + } + + // Read APlayer script + scriptData, err := resources.FS().Open("aplayer-share.js") + if err != nil { + log.Error(r.Context(), "Could not find aplayer-share.js", err) + http.Error(w, "Script not found", http.StatusInternalServerError) + return + } + defer scriptData.Close() + + scriptContent, err := io.ReadAll(scriptData) + if err != nil { + log.Error(r.Context(), "Error reading aplayer-share.js", err) + http.Error(w, "Error reading script", http.StatusInternalServerError) + return + } + + // Parse template + tmpl, err := template.New("aplayer").Parse(string(tmplContent)) + if err != nil { + log.Error(r.Context(), "Error parsing aplayer.html template", err) + http.Error(w, "Error parsing template", http.StatusInternalServerError) + return + } + + // Prepare share data for JSON + type aplayerTrack struct { + ID string `json:"id"` + Title string `json:"title"` + Artist string `json:"artist"` + Album string `json:"album"` + Duration float32 `json:"duration"` + UpdatedAt time.Time `json:"updatedAt"` + } + + type aplayerShareInfo struct { + ID string `json:"id"` + Description string `json:"description"` + Downloadable bool `json:"downloadable"` + Tracks []aplayerTrack `json:"tracks"` + ImageUrl string `json:"imageUrl"` + } + + shareData := aplayerShareInfo{ + ID: s.ID, + Description: s.Description, + Downloadable: s.Downloadable, + ImageUrl: s.ImageURL, + Tracks: slice.Map(s.Tracks, func(mf model.MediaFile) aplayerTrack { + return aplayerTrack{ + ID: mf.ID, + Title: mf.Title, + Artist: mf.Artist, + Album: mf.Album, + Duration: mf.Duration, + UpdatedAt: mf.UpdatedAt, + } + }), + } + + shareInfoJSON, err := json.Marshal(shareData) + if err != nil { + log.Error(r.Context(), "Error converting share data to JSON", err) + http.Error(w, "Error processing share data", http.StatusInternalServerError) + return + } + + // Prepare template data + description := s.Description + if description == "" { + description = str.SanitizeText(s.Contents) + } + if description == "" { + description = "Shared Music" + } + + baseURL := str.SanitizeText(conf.Server.BasePath) + + data := map[string]interface{}{ + "ShareDescription": description, + // #nosec G203 -- shareInfoJSON is generated by json.Marshal from server data, not user input + "ShareInfo": template.JS(shareInfoJSON), + // #nosec G203 -- scriptContent is from embedded resource file, not user input + "APlayerScript": template.JS(scriptContent), + "BaseURL": baseURL, + } + + // Render template + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + log.Error(r.Context(), "Error executing aplayer template", err) + http.Error(w, "Error rendering page", http.StatusInternalServerError) + return + } + + // Allow embedding in iframes for APlayer share pages + w.Header().Set("X-Frame-Options", "ALLOWALL") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(buf.Bytes()) +} + func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) { switch { case errors.Is(err, model.ErrExpired): diff --git a/server/public/public.go b/server/public/public.go index 5e3407c19..c2d398b3d 100644 --- a/server/public/public.go +++ b/server/public/public.go @@ -58,6 +58,9 @@ func (pub *Router) routes() http.Handler { r.HandleFunc("/d/{id}", pub.handleDownloads) } r.HandleFunc("/{id}/m3u", pub.handleM3U) + r.HandleFunc("/{id}/aplayer", pub.handleAPlayer) + r.HandleFunc("/aplayer/APlayer.min.css", pub.handleAPlayerCSS) + r.HandleFunc("/aplayer/APlayer.min.js", pub.handleAPlayerJS) r.HandleFunc("/{id}", pub.handleShares) r.HandleFunc("/", pub.handleShares) r.Handle("/*", pub.assetsHandler) diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go index 4eb756b98..f1e11b367 100644 --- a/server/subsonic/helpers_test.go +++ b/server/subsonic/helpers_test.go @@ -12,6 +12,7 @@ import ( "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/subsonic/responses" + "github.com/navidrome/navidrome/tests" "github.com/navidrome/navidrome/utils/req" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -489,6 +490,8 @@ var _ = Describe("helpers", func() { var ctx context.Context BeforeEach(func() { + ds := &tests.MockDataStore{} + auth.Init(ds) ctx = context.Background() conf.Server.Subsonic.EnableAverageRating = true }) diff --git a/ui/package-lock.json b/ui/package-lock.json index a9b83d76e..702be72b8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11285,4 +11285,4 @@ } } } -} +} \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index d4c149b23..9c3f5ee72 100644 --- a/ui/package.json +++ b/ui/package.json @@ -86,4 +86,4 @@ "rollup": "npm:@rollup/wasm-node" } } -} +} \ No newline at end of file diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index 6c6592178..b10ab4b8e 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -574,6 +574,23 @@ "delete_user_title": "Delete user '%{name}'", "delete_user_content": "Are you sure you want to delete this user and all their data (including playlists and preferences)?", "remove_missing_title": "Remove missing files", + "shareUrl": "Share URL", + "aplayerEmbedUrl": "APlayer Embed URL", + "navidromeMusicPlayer": "Navidrome Music Player", + "embedCode": "Embed Code", + "copyCode": "Copy Code", + "codeCopied": "Code copied to clipboard", + "embedTip": "Tip: Copy this code and paste it into your webpage HTML to use", + "floatingPlayerLeft": "Bottom Left Floating Player", + "floatingPlayerLeftDesc": "Collapsible floating player in bottom left corner, users can click to expand/collapse", + "fixedBottomPlayer": "Fixed Bottom Player", + "fixedBottomPlayerDesc": "Fixed at bottom of page, always visible (similar to MetingJS fixed mode)", + "basicIframe": "Basic iframe", + "basicIframeDesc": "Simple iframe embed, suitable for fixed position display", + "responsiveIframe": "Responsive iframe", + "responsiveIframeDesc": "16:9 responsive layout, adapts to different screen widths", + "floatingPlayerRight": "Bottom Right Floating Player", + "floatingPlayerRightDesc": "Collapsible floating player in bottom right corner (alternative position)", "remove_missing_content": "Are you sure you want to remove the selected missing files from the database? This will remove permanently any references to them, including their play counts and ratings.", "remove_all_missing_title": "Remove all missing files", "remove_all_missing_content": "Are you sure you want to remove all missing files from the database? This will permanently remove any references to them, including their play counts and ratings.", diff --git a/ui/src/share/EmbedCodeField.jsx b/ui/src/share/EmbedCodeField.jsx new file mode 100644 index 000000000..e0e1a000c --- /dev/null +++ b/ui/src/share/EmbedCodeField.jsx @@ -0,0 +1,344 @@ +import React, { useState } from 'react' +import { + Box, + Typography, + TextField, + IconButton, + Tabs, + Tab, + makeStyles, + Snackbar, +} from '@material-ui/core' +import { useTranslate } from 'react-admin' +import FileCopyIcon from '@material-ui/icons/FileCopy' + +const useStyles = makeStyles((theme) => ({ + root: { + marginBottom: theme.spacing(3), + }, + tabPanel: { + marginTop: theme.spacing(2), + }, + codeField: { + fontFamily: 'monospace', + fontSize: '12px', + '& .MuiInputBase-root': { + fontFamily: 'monospace', + fontSize: '12px', + }, + }, + copyButton: { + marginLeft: theme.spacing(1), + }, + header: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), + }, +})) + +const TabPanel = ({ children, value, index, ...other }) => { + return ( + + ) +} + +export const EmbedCodeField = ({ url, title = 'Music Player' }) => { + const classes = useStyles() + const translate = useTranslate() + const [tabValue, setTabValue] = useState(0) + const [snackbarOpen, setSnackbarOpen] = useState(false) + + const handleTabChange = (event, newValue) => { + setTabValue(newValue) + } + + const handleCopy = (text) => { + navigator.clipboard.writeText(text).then(() => { + setSnackbarOpen(true) + }) + } + + const handleSnackbarClose = () => { + setSnackbarOpen(false) + } + + // 基础 iframe 嵌入代码 + const iframeEmbed = `` + + // 响应式 iframe 嵌入代码 + const responsiveEmbed = `
+ +
` + + // 左下角悬浮播放器嵌入代码 - 参考 MetingJS 风格 + const floatingPlayerEmbed = ` + + + + +` + + // 固定底部播放器 - 参考 MetingJS fixed 模式 + const fixedBottomEmbed = ` + + +` + + // 右下角悬浮播放器(备选) + const floatingPlayerRightEmbed = floatingPlayerEmbed + .replace('left: 20px;', 'right: 20px;') + .replace('left: 10px;', 'right: 10px;') + + const embedOptions = [ + { + label: translate('message.floatingPlayerLeft'), + code: floatingPlayerEmbed, + description: translate('message.floatingPlayerLeftDesc'), + }, + { + label: translate('message.fixedBottomPlayer'), + code: fixedBottomEmbed, + description: translate('message.fixedBottomPlayerDesc'), + }, + { + label: translate('message.basicIframe'), + code: iframeEmbed, + description: translate('message.basicIframeDesc'), + }, + { + label: translate('message.responsiveIframe'), + code: responsiveEmbed, + description: translate('message.responsiveIframeDesc'), + }, + { + label: translate('message.floatingPlayerRight'), + code: floatingPlayerRightEmbed, + description: translate('message.floatingPlayerRightDesc'), + }, + ] + + return ( + + + {translate('message.embedCode')} + + + + {embedOptions.map((option, index) => ( + + ))} + + + {embedOptions.map((option, index) => ( + + + {option.description} + + + + 20 ? 20 : 12} + variant="outlined" + value={option.code} + className={classes.codeField} + InputProps={{ + readOnly: true, + }} + /> + handleCopy(option.code)} + color="primary" + size="small" + title={translate('message.copyCode')} + > + + + + + + {translate('message.embedTip')} + + + ))} + + + + ) +} diff --git a/ui/src/share/ShareEdit.jsx b/ui/src/share/ShareEdit.jsx index 2cf7f2df7..6eca08f1d 100644 --- a/ui/src/share/ShareEdit.jsx +++ b/ui/src/share/ShareEdit.jsx @@ -5,21 +5,31 @@ import { NumberField, SimpleForm, TextInput, + useTranslate, } from 'react-admin' -import { sharePlayerUrl } from '../utils' -import { Link } from '@material-ui/core' +import { sharePlayerUrl, shareAPlayerUrl } from '../utils' +import { + Link, + Box, + Typography, + Divider, + Accordion, + AccordionSummary, + AccordionDetails, +} from '@material-ui/core' import { DateField } from '../common' import config from '../config' +import { EmbedCodeField } from './EmbedCodeField' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' export const ShareEdit = (props) => { const { id, basePath, hasCreate, ...rest } = props + const translate = useTranslate() const url = sharePlayerUrl(id) + const aplayerUrl = shareAPlayerUrl(id) return ( - - {url} - {config.enableDownloads && } @@ -30,6 +40,44 @@ export const ShareEdit = (props) => { + + + } + aria-controls="share-urls-content" + id="share-urls-header" + > + + {translate('message.shareUrl')} &{' '} + {translate('message.aplayerEmbedUrl')} + + + + + + {translate('message.shareUrl')} + + + {url} + + + + + {translate('message.aplayerEmbedUrl')} + + + {aplayerUrl} + + + + + + + + ) diff --git a/ui/src/utils/urls.js b/ui/src/utils/urls.js index 80207fe83..a130c359f 100644 --- a/ui/src/utils/urls.js +++ b/ui/src/utils/urls.js @@ -25,6 +25,14 @@ export const sharePlayerUrl = (id) => { return url.href } +export const shareAPlayerUrl = (id) => { + const url = new URL( + shareUrl(config.publicBaseUrl + '/' + id + '/aplayer'), + window.location.href, + ) + return url.href +} + export const shareStreamUrl = (id) => { return shareUrl(config.publicBaseUrl + '/s/' + id) }