├── 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 |
--------------------------------------------------------------------------------