Разработан веб-сервер и расширение для браузеров на базе Chromium
This commit is contained in:
commit
beb1559cd4
14 files changed
+490
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
const mutationObserverConfig = {
|
||||||
|
attributes: true,
|
||||||
|
childList: false,
|
||||||
|
characterData: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectWebSocket = () => {
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/ws/v1/plugin');
|
||||||
|
const progressElement = document.querySelector('#progress-bar');
|
||||||
|
const mutationObserver = new MutationObserver(() => {
|
||||||
|
const promise = new Promise(resolve => {
|
||||||
|
const playerBarElement = document.querySelector('ytmusic-player-bar');
|
||||||
|
const metadataElement = playerBarElement.querySelector('.middle-controls > .content-info-wrapper');
|
||||||
|
const titleElement = metadataElement.querySelector(`yt-formatted-string.title`);
|
||||||
|
const subtitleElement = metadataElement.querySelector('span.subtitle > yt-formatted-string.byline');
|
||||||
|
const imageElement = playerBarElement.querySelector('img');
|
||||||
|
if (titleElement.textContent === '' || subtitleElement.length === 0) return;
|
||||||
|
const data = {
|
||||||
|
type: 'music',
|
||||||
|
attributes: {
|
||||||
|
title: titleElement.textContent,
|
||||||
|
artists: subtitleElement.title.split('•')[0].trim(),
|
||||||
|
progress: progressElement.ariaValueNow / progressElement.ariaValueMax,
|
||||||
|
image: imageElement.src,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(data));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
promise.then();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
mutationObserver.observe(progressElement, mutationObserverConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
connectWebSocket();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
connectWebSocket();
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "YT Music Live",
|
||||||
|
"description": "YT Music Live",
|
||||||
|
"version": "1.0",
|
||||||
|
"manifest_version": 3,
|
||||||
|
"action": {
|
||||||
|
"default_icon": "icon.png"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": [
|
||||||
|
"main.js"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"*://music.youtube.com/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"scripting"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .main import app
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
|
from fastapi.responses import Response, HTMLResponse, FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.mount(
|
||||||
|
path='/static',
|
||||||
|
app=StaticFiles(
|
||||||
|
directory=os.path.join(
|
||||||
|
config.Main.working_directory,
|
||||||
|
'static',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(
|
||||||
|
searchpath=os.path.join(
|
||||||
|
'.',
|
||||||
|
'templates',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enable_async=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.connections: list[WebSocket] = list[WebSocket]()
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.connections.remove(websocket)
|
||||||
|
|
||||||
|
async def broadcast(self, data: dict):
|
||||||
|
for connection in self.connections:
|
||||||
|
asyncio.ensure_future(connection.send_json(data))
|
||||||
|
|
||||||
|
|
||||||
|
overlay_manager = ConnectionManager()
|
||||||
|
plugin_manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
@app.options(
|
||||||
|
path='/api/v1/overlay',
|
||||||
|
)
|
||||||
|
async def function():
|
||||||
|
return Response(
|
||||||
|
headers={
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': '*',
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
path='/overlay',
|
||||||
|
)
|
||||||
|
async def function(
|
||||||
|
justify_content: str = Query(
|
||||||
|
default='end',
|
||||||
|
alias='justify-content',
|
||||||
|
),
|
||||||
|
align_items: str = Query(
|
||||||
|
default='end',
|
||||||
|
alias='align-items',
|
||||||
|
),
|
||||||
|
):
|
||||||
|
template = env.get_template('overlay.jinja2')
|
||||||
|
return HTMLResponse(
|
||||||
|
content=await template.render_async(
|
||||||
|
justify_content=justify_content,
|
||||||
|
align_items=align_items,
|
||||||
|
),
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
path='/image',
|
||||||
|
)
|
||||||
|
async def function(
|
||||||
|
src: str = Query(
|
||||||
|
default=...,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
image_name = hashlib.sha1(src.encode('ascii')).hexdigest()
|
||||||
|
image_path = os.path.join(
|
||||||
|
config.Main.images_directory,
|
||||||
|
image_name,
|
||||||
|
)
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
os.makedirs(
|
||||||
|
name=config.Main.images_directory,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(src) as response:
|
||||||
|
if not response.ok:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=response.status,
|
||||||
|
)
|
||||||
|
with open(
|
||||||
|
file=image_path,
|
||||||
|
mode='wb',
|
||||||
|
) as file:
|
||||||
|
file.write(await response.read())
|
||||||
|
return FileResponse(
|
||||||
|
path=image_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket('/ws/v1/overlay')
|
||||||
|
async def function(
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
await overlay_manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_json()
|
||||||
|
await plugin_manager.broadcast(data)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
overlay_manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket('/ws/v1/plugin')
|
||||||
|
async def function(
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
await plugin_manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_json()
|
||||||
|
await overlay_manager.broadcast(data)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
plugin_manager.disconnect(websocket)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[Main]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 8000
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .main import Main
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from configparser import RawConfigParser
|
||||||
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
config = RawConfigParser()
|
||||||
|
config.optionxform = str
|
||||||
|
config.read(
|
||||||
|
filenames='./config.ini',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Section(Enum):
|
||||||
|
main = 'Main'
|
||||||
|
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
host = config.get(
|
||||||
|
section=Section.main.value,
|
||||||
|
option='host',
|
||||||
|
fallback='127.0.0.1',
|
||||||
|
)
|
||||||
|
port = config.getint(
|
||||||
|
section=Section.main.value,
|
||||||
|
option='port',
|
||||||
|
fallback=8000,
|
||||||
|
)
|
||||||
|
working_directory = config.get(
|
||||||
|
section=Section.main.value,
|
||||||
|
option='working_directory',
|
||||||
|
fallback=os.getcwd(),
|
||||||
|
)
|
||||||
|
images_directory = config.get(
|
||||||
|
section=Section.main.value,
|
||||||
|
option='images_directory',
|
||||||
|
fallback=os.path.join(
|
||||||
|
os.getenv('APPDATA'),
|
||||||
|
'csasq',
|
||||||
|
'YT Music Live',
|
||||||
|
'cache',
|
||||||
|
'covers',
|
||||||
|
),
|
||||||
|
)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
import shutil
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from api import app
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
uvicorn.run(
|
||||||
|
app=app,
|
||||||
|
host=config.Main.host,
|
||||||
|
port=config.Main.port,
|
||||||
|
)
|
||||||
|
except InterruptedError:
|
||||||
|
shutil.rmtree(config.Main.images_directory)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
aiohttp
|
||||||
|
fastapi
|
||||||
|
jinja2
|
||||||
|
Nuitka
|
||||||
|
uvicorn
|
||||||
|
websockets
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(t||self).ColorThief=r()}(this,function(){if(!t)var t={map:function(t,r){var n={};return r?t.map(function(t,o){return n.index=o,r.call(n,t)}):t.slice()},naturalOrder:function(t,r){return t<r?-1:t>r?1:0},sum:function(t,r){var n={};return t.reduce(r?function(t,o,e){return n.index=e,t+r.call(n,o)}:function(t,r){return t+r},0)},max:function(r,n){return Math.max.apply(null,n?t.map(r,n):r)}};var r=function(){var r=5,n=8-r,o=1e3;function e(t,n,o){return(t<<2*r)+(n<<r)+o}function i(t){var r=[],n=!1;function o(){r.sort(t),n=!0}return{push:function(t){r.push(t),n=!1},peek:function(t){return n||o(),void 0===t&&(t=r.length-1),r[t]},pop:function(){return n||o(),r.pop()},size:function(){return r.length},map:function(t){return r.map(t)},debug:function(){return n||o(),r}}}function u(t,r,n,o,e,i,u){var a=this;a.r1=t,a.r2=r,a.g1=n,a.g2=o,a.b1=e,a.b2=i,a.histo=u}function a(){this.vboxes=new i(function(r,n){return t.naturalOrder(r.vbox.count()*r.vbox.volume(),n.vbox.count()*n.vbox.volume())})}function c(r,n){if(n.count()){var o=n.r2-n.r1+1,i=n.g2-n.g1+1,u=t.max([o,i,n.b2-n.b1+1]);if(1==n.count())return[n.copy()];var a,c,f,s,l=0,h=[],v=[];if(u==o)for(a=n.r1;a<=n.r2;a++){for(s=0,c=n.g1;c<=n.g2;c++)for(f=n.b1;f<=n.b2;f++)s+=r[e(a,c,f)]||0;h[a]=l+=s}else if(u==i)for(a=n.g1;a<=n.g2;a++){for(s=0,c=n.r1;c<=n.r2;c++)for(f=n.b1;f<=n.b2;f++)s+=r[e(c,a,f)]||0;h[a]=l+=s}else for(a=n.b1;a<=n.b2;a++){for(s=0,c=n.r1;c<=n.r2;c++)for(f=n.g1;f<=n.g2;f++)s+=r[e(c,f,a)]||0;h[a]=l+=s}return h.forEach(function(t,r){v[r]=l-t}),function(t){var r,o,e,i,u,c=t+"1",f=t+"2",s=0;for(a=n[c];a<=n[f];a++)if(h[a]>l/2){for(e=n.copy(),i=n.copy(),u=(r=a-n[c])<=(o=n[f]-a)?Math.min(n[f]-1,~~(a+o/2)):Math.max(n[c],~~(a-1-r/2));!h[u];)u++;for(s=v[u];!s&&h[u-1];)s=v[--u];return e[f]=u,i[c]=e[f]+1,[e,i]}}(u==o?"r":u==i?"g":"b")}}return u.prototype={volume:function(t){var r=this;return r._volume&&!t||(r._volume=(r.r2-r.r1+1)*(r.g2-r.g1+1)*(r.b2-r.b1+1)),r._volume},count:function(t){var r=this,n=r.histo;if(!r._count_set||t){var o,i,u,a=0;for(o=r.r1;o<=r.r2;o++)for(i=r.g1;i<=r.g2;i++)for(u=r.b1;u<=r.b2;u++)a+=n[e(o,i,u)]||0;r._count=a,r._count_set=!0}return r._count},copy:function(){var t=this;return new u(t.r1,t.r2,t.g1,t.g2,t.b1,t.b2,t.histo)},avg:function(t){var n=this,o=n.histo;if(!n._avg||t){var i,u,a,c,f=0,s=1<<8-r,l=0,h=0,v=0;for(u=n.r1;u<=n.r2;u++)for(a=n.g1;a<=n.g2;a++)for(c=n.b1;c<=n.b2;c++)f+=i=o[e(u,a,c)]||0,l+=i*(u+.5)*s,h+=i*(a+.5)*s,v+=i*(c+.5)*s;n._avg=f?[~~(l/f),~~(h/f),~~(v/f)]:[~~(s*(n.r1+n.r2+1)/2),~~(s*(n.g1+n.g2+1)/2),~~(s*(n.b1+n.b2+1)/2)]}return n._avg},contains:function(t){var r=this,o=t[0]>>n;return gval=t[1]>>n,bval=t[2]>>n,o>=r.r1&&o<=r.r2&&gval>=r.g1&&gval<=r.g2&&bval>=r.b1&&bval<=r.b2}},a.prototype={push:function(t){this.vboxes.push({vbox:t,color:t.avg()})},palette:function(){return this.vboxes.map(function(t){return t.color})},size:function(){return this.vboxes.size()},map:function(t){for(var r=this.vboxes,n=0;n<r.size();n++)if(r.peek(n).vbox.contains(t))return r.peek(n).color;return this.nearest(t)},nearest:function(t){for(var r,n,o,e=this.vboxes,i=0;i<e.size();i++)((n=Math.sqrt(Math.pow(t[0]-e.peek(i).color[0],2)+Math.pow(t[1]-e.peek(i).color[1],2)+Math.pow(t[2]-e.peek(i).color[2],2)))<r||void 0===r)&&(r=n,o=e.peek(i).color);return o},forcebw:function(){var r=this.vboxes;r.sort(function(r,n){return t.naturalOrder(t.sum(r.color),t.sum(n.color))});var n=r[0].color;n[0]<5&&n[1]<5&&n[2]<5&&(r[0].color=[0,0,0]);var o=r.length-1,e=r[o].color;e[0]>251&&e[1]>251&&e[2]>251&&(r[o].color=[255,255,255])}},{quantize:function(f,s){if(!f.length||s<2||s>256)return!1;var l=function(t){var o,i=new Array(1<<3*r);return t.forEach(function(t){o=e(t[0]>>n,t[1]>>n,t[2]>>n),i[o]=(i[o]||0)+1}),i}(f);l.forEach(function(){});var h=function(t,r){var o,e,i,a=1e6,c=0,f=1e6,s=0,l=1e6,h=0;return t.forEach(function(t){(o=t[0]>>n)<a?a=o:o>c&&(c=o),(e=t[1]>>n)<f?f=e:e>s&&(s=e),(i=t[2]>>n)<l?l=i:i>h&&(h=i)}),new u(a,c,f,s,l,h,r)}(f,l),v=new i(function(r,n){return t.naturalOrder(r.count(),n.count())});function g(t,r){for(var n,e=t.size(),i=0;i<o;){if(e>=r)return;if(i++>o)return;if((n=t.pop()).count()){var u=c(l,n),a=u[0],f=u[1];if(!a)return;t.push(a),f&&(t.push(f),e++)}else t.push(n),i++}}v.push(h),g(v,.75*s);for(var p=new i(function(r,n){return t.naturalOrder(r.count()*r.volume(),n.count()*n.volume())});v.size();)p.push(v.pop());g(p,s);for(var b=new a;p.size();)b.push(p.pop());return b}}}().quantize,n=function(t){this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.width=this.canvas.width=t.naturalWidth,this.height=this.canvas.height=t.naturalHeight,this.context.drawImage(t,0,0,this.width,this.height)};n.prototype.getImageData=function(){return this.context.getImageData(0,0,this.width,this.height)};var o=function(){};return o.prototype.getColor=function(t,r){return void 0===r&&(r=10),this.getPalette(t,5Line truncated
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const colorThief = new ColorThief();
|
||||||
|
const musicElement = document.querySelector('#music');
|
||||||
|
const progressBarElement = musicElement.querySelector('#music > .progress-bar');
|
||||||
|
const titleElement = progressBarElement.querySelector('#music > .progress-bar > .data > .title');
|
||||||
|
const artistsElement = progressBarElement.querySelector('#music > .progress-bar > .data > .artists');
|
||||||
|
const coverElement = progressBarElement.querySelector('#music > .progress-bar > .cover');
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
'target': (attributes) => {
|
||||||
|
const target = document.getElementById(attributes.id);
|
||||||
|
target.style.setProperty('--progress-size', `${attributes.progress}`);
|
||||||
|
},
|
||||||
|
'music': (attributes) => {
|
||||||
|
titleElement.textContent = attributes.title;
|
||||||
|
artistsElement.textContent = attributes.artists;
|
||||||
|
musicElement.style.setProperty('--progress-size', `${(1 - attributes.progress) * 100}%`);
|
||||||
|
const url = new URL(location);
|
||||||
|
url.pathname = '/image';
|
||||||
|
const urlSearchParams = new URLSearchParams();
|
||||||
|
urlSearchParams.set('src', attributes.image);
|
||||||
|
url.search = urlSearchParams.toString();
|
||||||
|
coverElement.src = url;
|
||||||
|
const colors = colorThief.getPalette(coverElement, 2);
|
||||||
|
progressBarElement.style.setProperty('--primary-color', `rgb(${colors[0][0]}, ${colors[0][1]}, ${colors[0][2]})`);
|
||||||
|
progressBarElement.style.setProperty('--secondary-color', `rgb(${colors[1][0]}, ${colors[1][1]}, ${colors[1][2]})`);
|
||||||
|
musicElement.classList.remove('hidden');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectWebSocket = () => {
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/ws/v1/overlay');
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
handlers[data.type](data.attributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => connectWebSocket();
|
||||||
|
};
|
||||||
|
|
||||||
|
connectWebSocket();
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
@property --primary-color {
|
||||||
|
syntax: '<color>';
|
||||||
|
initial-value: white;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --secondary-color {
|
||||||
|
syntax: '<color>';
|
||||||
|
initial-value: white;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--text-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
|
||||||
|
--box-shadow: 0 .5rem .75rem .375rem rgba(0, 0, 0, .15), 0 .25rem .25rem rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Exo 2', sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > * {
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > *:not(:first-child) {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > *.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > * > .title {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
color: white;
|
||||||
|
text-shadow: var(--text-shadow);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
--progress: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: .5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 28rem;
|
||||||
|
background-image: linear-gradient(225deg, var(--primary-color), var(--secondary-color));
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
border-radius: .5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
opacity .4s,
|
||||||
|
--primary-color 1s,
|
||||||
|
--secondary-color 1s;
|
||||||
|
--progress-size: inherit;
|
||||||
|
--progress-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: var(--progress-size);
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0, 0, 0, .3);
|
||||||
|
transition:
|
||||||
|
right 1s,
|
||||||
|
background-color 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar > .data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 1rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar > .data > .title,
|
||||||
|
#music > .progress-bar > .data > .artists {
|
||||||
|
color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar > .data > .title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar > .data > .artists {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music > .progress-bar > .cover {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
object-fit: cover;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru-RU">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title></title>
|
||||||
|
<link rel="stylesheet" href="/static/styles/main.css" />
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
justify-content: {{ justify_content }};
|
||||||
|
align-items: {{ align_items }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="/static/scripts/color-thief.umd.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
<div id="music" class="hidden">
|
||||||
|
<span class="title">Сейчас играет</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<img class="cover" />
|
||||||
|
<div class="data">
|
||||||
|
<span class="title"></span>
|
||||||
|
<span class="artists"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script src="/static/scripts/main.js"></script>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user