├── README.md ├── LICENSE ├── Plugin.php ├── SQPlayer.css └── SQPlayer.js /README.md: -------------------------------------------------------------------------------- 1 | # Square Player 2 | 3 | 一个简洁到极致的单曲播放器,基于 ES6 标准开发的试水之作。 4 | 5 | ## 使用方法 6 | 7 | 1. `Star` 本项目 8 | 2. 从这里 [下载](https://github.com/Dreamer-Paul/Square-Player/archive/master.zip) 源码 9 | 3. 可使用本地、网易云音乐两种方式食用,参照 [文档站](https://docs.paul.ren/square) 即可快速部署完成 10 | 4. 本项目提供 Typecho 插件版本,如需使用请先将下载后得到的文件夹从 `Square-Player-master` 重命名为 `SQP` 11 | 12 | ## 开源协议 13 | 14 | 本项目采用 MIT 开源协议进行授权,请保留原作者的版权注释(CSS、JS 文件) 15 | 16 | 原创不易!如果喜欢本项目,请 `Star` 它以示对我的支持~ 17 | 18 | 同时欢迎前往 [保罗的小窝](https://paul.ren/donate) 为我提供赞助,谢谢您! 19 | 20 | ## 感谢 21 | 22 | 感谢来自开源社区提供的解决方案,如果没有它们,Square Player 可能还无法如此完善~ 23 | 24 | - [Meting](https://github.com/metowolf/Meting) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2019 Dreamer-Paul 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. -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | header = array('SQP_Plugin', 'header'); 16 | Typecho_Plugin::factory('Widget_Archive') -> footer = array('SQP_Plugin', 'footer'); 17 | } 18 | 19 | /* 禁用插件方法 */ 20 | public static function deactivate(){} 21 | 22 | /* 插件配置方法 */ 23 | public static function config(Typecho_Widget_Helper_Form $form){} 24 | 25 | /* 个人用户的配置方法 */ 26 | public static function personalConfig(Typecho_Widget_Helper_Form $form){} 27 | 28 | /* 插件实现方法 */ 29 | public static function header(){ 30 | if(Typecho_Widget::widget('Widget_Archive') -> is("post")){ 31 | echo ''; 32 | } 33 | } 34 | 35 | public static function footer(){ 36 | if(Typecho_Widget::widget('Widget_Archive') -> is("post")){ 37 | echo ''; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /SQPlayer.css: -------------------------------------------------------------------------------- 1 | /* ---- 2 | 3 | # Square Player 4 | # By: Dreamer-Paul 5 | # Last Update: 2024.4.20 6 | 7 | 一个简洁到极致的单曲播放器。 8 | 9 | 本代码为奇趣保罗原创,并遵守 MIT 开源协议。欢迎访问我的博客:https://paugram.com 10 | 11 | ---- */ 12 | 13 | sqp { 14 | width: 8em; 15 | height: 8em; 16 | color: #fff; 17 | float: right; 18 | display: block; 19 | overflow: hidden; 20 | user-select: none; 21 | position: relative; 22 | border-radius: 1em; 23 | margin: 0 0 1em 1em; 24 | background: #ccc center/cover no-repeat; 25 | } 26 | sqp[left] { 27 | float: left; 28 | margin: 0 1em 1em 0; 29 | } 30 | 31 | sqp .sqp-info { 32 | left: 0; 33 | right: 0; 34 | bottom: 0; 35 | padding: .75em 0; 36 | font-size: .75em; 37 | position: absolute; 38 | backdrop-filter: blur(2px); 39 | background-color: rgba(0, 0, 0, .4); 40 | } 41 | 42 | sqp .sqp-title { 43 | padding: 0 1em; 44 | white-space: nowrap; 45 | display: inline-block; 46 | } 47 | 48 | sqp .sqp-toggle { 49 | width: 2em; 50 | height: 2em; 51 | opacity: .8; 52 | cursor: pointer; 53 | position: relative; 54 | border-radius: 66%; 55 | display: inline-block; 56 | backdrop-filter: blur(2px); 57 | box-shadow: 0 0 0 .25em rgba(0, 0, 0, .4); 58 | transform: translate(calc(4em - 50%), 100%); 59 | transition: opacity .3s, width .3s, height .3s, transform .3s; 60 | background: #fff center/1em no-repeat; 61 | background-image: url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAxMCAxMCI+DQo8cG9seWdvbiBmaWxsPSIjRkZGRkZGIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSItNDEuMywyOC41IDIsLTQ2LjUgNDUuMywyOC41ICIvPg0KPHBvbHlnb24gcG9pbnRzPSIyLDEuNSA4LDUgMiw4LjUgIi8+DQo8L3N2Zz4NCg==) 62 | } 63 | 64 | sqp .sqp-toggle:hover { 65 | opacity: 1; 66 | } 67 | 68 | sqp .sqp-toggle.playing { 69 | width: 1.5em; 70 | height: 1.5em; 71 | transform: translate(.5em, .5em); 72 | background-image: url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAxMCAxMCI+DQo8cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMiIgaGVpZ2h0PSI4Ii8+DQo8cmVjdCB4PSI2IiB5PSIxIiB3aWR0aD0iMiIgaGVpZ2h0PSI4Ii8+DQo8L3N2Zz4NCg==); 73 | } 74 | -------------------------------------------------------------------------------- /SQPlayer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* ---- 4 | 5 | # Square Player 6 | # By: Dreamer-Paul 7 | # Last Update: 2024.4.20 8 | 9 | 一个简洁到极致的单曲播放器。 10 | 11 | 本代码为奇趣保罗原创,并遵守 MIT 开源协议。欢迎访问我的博客:https://paugram.com 12 | 13 | ---- */ 14 | 15 | window._SQPPlayers = []; 16 | 17 | class SQPlayer { 18 | key = ""; 19 | elements = undefined; 20 | 21 | constructor(wrapper, key, set) { 22 | this.key = key; 23 | this.elements = { 24 | wrap: wrapper, 25 | player: new Audio(), 26 | info: this.creator("div", { className: "info" }), 27 | title: this.creator("span", { className: "title", content: "加载中..." }), 28 | toggle: this.creator("div", { className: "toggle" }) 29 | }; 30 | 31 | this.elements.wrap.setAttribute("loaded", ""); 32 | this.elements.player.setAttribute("preload", "none"); 33 | 34 | if (wrapper.dataset.cid) { 35 | this.setupByCloudMusic(wrapper.dataset.cid, set.server); 36 | } 37 | else { 38 | this.setup(wrapper.dataset); 39 | } 40 | 41 | this.elements.info.appendChild(this.elements.title); 42 | this.elements.wrap.appendChild(this.elements.info); 43 | this.elements.wrap.appendChild(this.elements.toggle); 44 | } 45 | 46 | // 切换 47 | toggle = () => this.events.onToggle(); 48 | 49 | // 播放 50 | play = () => { 51 | this.elements.player.play(); 52 | 53 | _SQPPlayers.forEach(item => { 54 | if (item.key !== this.key) item.pause(); 55 | }); 56 | } 57 | 58 | // 暂停 59 | pause = () => { 60 | this.elements.player.pause(); 61 | } 62 | 63 | // 元素创建器 64 | creator(tag, attr) { 65 | const el = document.createElement(tag); 66 | 67 | if (attr?.className) { 68 | el.className = `sqp-${attr.className}`; 69 | } 70 | 71 | if (attr?.content) { 72 | el.innerHTML = attr.content; 73 | } 74 | 75 | return el; 76 | } 77 | 78 | // 事件 79 | events = { 80 | onToggle: () => { 81 | this.elements.player.paused ? this.play() : this.pause(); 82 | }, 83 | onPlay: () => { 84 | this.elements.toggle.classList.add("playing"); 85 | }, 86 | onPause: () => { 87 | this.elements.toggle.classList.remove("playing"); 88 | }, 89 | } 90 | 91 | // 修改元素的操作 92 | modify = { 93 | updateTitleText: (nextTitle) => { 94 | const fontSize = Number(window.getComputedStyle(document.querySelector("html")).fontSize.replace("px", "")); 95 | 96 | const el = this.elements.title; 97 | el.innerText = nextTitle; 98 | 99 | const offset = el.offsetWidth - (fontSize * 8); 100 | const duration = parseInt(el.offsetWidth / 30) * 1000; 101 | 102 | if (offset > 0) { 103 | el.animate([ 104 | { transform: "translateX(0)" }, 105 | { transform: `translateX(${-offset}px)` }, 106 | { transform: "translateX(0)" }, 107 | ], { 108 | duration, 109 | iterations: Infinity, 110 | }); 111 | } 112 | }, 113 | } 114 | 115 | // 设置播放器 116 | setup = (item) => { 117 | // 播放器主体初始化 118 | let titleText = "未知标题"; 119 | 120 | if (item.artist && item.title) { 121 | titleText = `${item.title} - ${item.artist}`; 122 | } 123 | else if (item.title) { 124 | titleText = item.title; 125 | } 126 | 127 | if (item.link) { 128 | this.elements.player.src = item.link; 129 | } 130 | else { 131 | titleText = "无效文件路径"; 132 | console.error("SQP: Error, No files to play!"); 133 | } 134 | 135 | this.modify.updateTitleText(titleText); 136 | 137 | if (item.cover) { 138 | this.elements.wrap.style.backgroundImage = `url(${item.cover})`; 139 | } 140 | 141 | this.elements.toggle.addEventListener("click", this.events.onToggle); 142 | this.elements.player.addEventListener("play", this.events.onPlay); 143 | this.elements.player.addEventListener("pause", this.events.onPause); 144 | } 145 | 146 | // 销毁 147 | destroy = () => { 148 | this.elements.player.pause(); 149 | this.elements.toggle.removeEventListener("click", this.events.onToggle); 150 | this.elements.player.removeEventListener("play", this.events.onPlay); 151 | this.elements.player.removeEventListener("pause", this.events.onPause); 152 | 153 | this.elements.wrap.remove(); 154 | this.elements.wrap = undefined; 155 | this.elements = undefined; 156 | } 157 | 158 | setupByCloudMusic = (cid, server) => { 159 | const getData = { 160 | "meto": () => ( 161 | fetch(`https://api.i-meto.com/meting/api?server=netease&id=${cid}`).then( 162 | (res) => res.json() 163 | ).then((items) => { 164 | const item = items[0]; 165 | 166 | if (!item) { 167 | throw new Error("返回数据为空"); 168 | } 169 | 170 | this.setup({ 171 | title: item.title, 172 | artist: item.author, 173 | cover: item.pic, 174 | link: item.url 175 | }); 176 | }) 177 | ), 178 | "paul": () => ( 179 | fetch(`https://api.paugram.com/netease/?id=${cid}`).then( 180 | (res) => res.json() 181 | ).then((item) => { 182 | this.setup(item); 183 | }) 184 | ), 185 | }; 186 | 187 | if (server in getData) { 188 | getData[server]().catch((err) => { 189 | this.modify.updateTitleText(`获取数据异常:${err.message}`); 190 | }); 191 | } 192 | } 193 | } 194 | 195 | console.log("%c Square Player %c https://paugram.com ", "color: #fff; margin: 1em 0; padding: 5px 0; background: #1875b3;", "margin: 1em 0; padding: 5px 0; background: #efefef;"); 196 | 197 | class SQP_Extend { 198 | constructor(settings) { 199 | this.settings = settings; 200 | this.init(); 201 | } 202 | 203 | init = () => { 204 | this.wrapper = document.querySelectorAll("sqp"); 205 | 206 | this.wrapper.forEach((item, key) => { 207 | if (!item.hasAttribute("loaded")) { 208 | _SQPPlayers.push(new SQPlayer(item, key + new Date().getTime(), this.settings)); 209 | } 210 | }); 211 | } 212 | 213 | destroy = () => { 214 | while (_SQPPlayers.length > 0) { 215 | _SQPPlayers[0].destroy(); 216 | _SQPPlayers.splice(_SQPPlayers[0], 1); 217 | } 218 | } 219 | } 220 | 221 | window._SQP_Extend = new SQP_Extend({ 222 | server: "meto" 223 | }); 224 | --------------------------------------------------------------------------------