Add base files from AME

This commit is contained in:
cryptofyre 2021-11-19 17:02:28 -06:00
parent e8f3881513
commit e3c3bded3a
129 changed files with 19056 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z"/></svg>

After

Width:  |  Height:  |  Size: 521 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M11.5 280.6l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2zm256 0l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2z"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"/></svg>

After

Width:  |  Height:  |  Size: 831 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z"/></svg>

After

Width:  |  Height:  |  Size: 487 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>

After

Width:  |  Height:  |  Size: 384 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M464 32H336c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48zm-288 0H48C21.5 32 0 53.5 0 80v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48z"/></svg>

After

Width:  |  Height:  |  Size: 640 B

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;" xml:space="preserve">
<g>
<path fill="white" d="M42,12H20.414l7.293-7.293c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0l-8.999,8.999
c-0.093,0.092-0.166,0.203-0.217,0.326c-0.101,0.244-0.101,0.52,0,0.764c0.051,0.123,0.124,0.234,0.217,0.326l8.999,8.999
C26.488,22.902,26.744,23,27,23s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414L20.414,14H42c8.822,0,16,7.178,16,16
c0,4.252-1.668,8.264-4.696,11.295c-0.391,0.391-0.391,1.024,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
C58.124,39.3,60,34.786,60,30C60,20.075,51.925,12,42,12z"/>
<path fill="white" d="M35.707,37.293c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414L41.586,46H18C9.178,46,2,38.822,2,30
c0-3.783,1.359-7.46,3.828-10.354c0.358-0.421,0.309-1.052-0.111-1.41c-0.419-0.359-1.052-0.31-1.41,0.111
C1.529,21.604,0,25.741,0,30c0,9.925,8.075,18,18,18h23.586l-7.293,7.293c-0.391,0.391-0.391,1.023,0,1.414
C34.488,56.902,34.744,57,35,57s0.512-0.098,0.707-0.293l9-9c0.391-0.391,0.391-1.023,0-1.414L35.707,37.293z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"/></svg>

After

Width:  |  Height:  |  Size: 618 B

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 230.055 230.055" style="enable-background:new 0 0 230.055 230.055;" xml:space="preserve">
<path fill="white" d="M199.419,124.497c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636h-8.406
c-8.177,0-16.151-2.871-22.453-8.083l-32.346-26.751l32.345-26.751c6.303-5.212,14.277-8.083,22.454-8.083h8.406L186.69,92.83
c-3.515,3.515-3.515,9.213,0,12.728c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28
c3.515-3.515,3.515-9.213,0-12.728l-28-28c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636
h-8.406c-12.354,0-24.403,4.337-33.926,12.211L122,103.348L82.564,70.733c-6.658-5.507-15.084-8.54-23.724-8.54H9
c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c4.462,0,8.813,1.566,12.252,4.411l36.786,30.423L71.094,145.45
c-3.439,2.844-7.791,4.411-12.253,4.411H9c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c8.64,0,17.065-3.033,23.725-8.54L122,126.707
l34.996,28.943c9.521,7.875,21.57,12.211,33.925,12.211h8.406l-12.637,12.636c-3.515,3.515-3.515,9.213,0,12.728
c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28c3.515-3.515,3.515-9.213,0-12.728L199.419,124.497z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 72.04L126.06 161H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V89.02c0-21.47-25.96-31.98-40.97-16.98zm123.2 108.08c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 229.28 336 242.62 336 257c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.87z"/></svg>

After

Width:  |  Height:  |  Size: 710 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zm233.32-51.08c-11.17-7.33-26.18-4.24-33.51 6.95-7.34 11.17-4.22 26.18 6.95 33.51 66.27 43.49 105.82 116.6 105.82 195.58 0 78.98-39.55 152.09-105.82 195.58-11.17 7.32-14.29 22.34-6.95 33.5 7.04 10.71 21.93 14.56 33.51 6.95C528.27 439.58 576 351.33 576 256S528.27 72.43 448.35 19.97zM480 256c0-63.53-32.06-121.94-85.77-156.24-11.19-7.14-26.03-3.82-33.12 7.46s-3.78 26.21 7.41 33.36C408.27 165.97 432 209.11 432 256s-23.73 90.03-63.48 115.42c-11.19 7.14-14.5 22.07-7.41 33.36 6.51 10.36 21.12 15.14 33.12 7.46C447.94 377.94 480 319.54 480 256zm-141.77-76.87c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 228.28 336 241.63 336 256c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.86z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -0,0 +1,626 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Web Remote</title>
<link rel="stylesheet" href="style.css?v=2">
<script src="vue.js"></script>
<script src="sortable.min.js"></script>
<script src="vuedraggable.umd.min.js"></script>
<link rel="manifest" href="./manifest.json?v=2">
</head>
<body oncontextmenu="return false;">
<div id="app" :style="{'--artwork': getAlbumArtUrl()}">
<template v-if="connectedState == 1">
<transition name="wpfade">
<div class="md-container md-container_panel player-panel" v-if="screen == 'player'">
<div class="player_top">
<div class="md-body player-artwork-container">
<div class="media-artwork" :class="artworkPlaying()"
:style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'lyrics'">
<div class="md-header" style="width:100%;">
<div class="list-entry" v-if="false">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body" style="width:100%;">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active"
@click="player.lowerPanelState = 'controls'"></button>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'controls'">
<div class="md-footer">
<div class="row player-track-info">
<div class="col nopadding">
<div class="player-song-title">
{{ player.currentMediaItem.name }}
</div>
<div class="player-song-artist" @click="searchArtist()">
{{ player.currentMediaItem.artistName }}
</div>
</div>
<div class="col-auto nopadding player-more-container" v-if="false" style="">
<button @click="player.songActions = true;" class="player-more-button">...</button>
</div>
</div>
</div>
<div class="md-footer">
<input type="range" min="0"
:value="player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime"
:max="player.currentMediaItem.durationInMillis" class="web-slider playback-slider"
@input="seekTo($event.target.value)">
<div class="row nopadding player-duration-container" style="width: 90%;margin: 0 auto;">
<div class="col nopadding player-duration-time" style="text-align:left">
{{ parseTime(player.currentMediaItem.durationInMillis -
player.currentMediaItem.remainingTime) }}
</div>
<div class="col nopadding player-duration-time" style="text-align:right">
-{{ parseTime(player.currentMediaItem.remainingTime) }}
</div>
</div>
</div>
<div class="md-footer playback-buttons">
<button class="md-btn playback-button--small repeat" @click="repeat()"
v-if="player.currentMediaItem.repeatMode == 0"></button>
<button class="md-btn playback-button--small repeat active" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 2"></button>
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 1"></button>
<button class="md-btn playback-button previous" @click="previous()"></button>
<button class="md-btn playback-button pause" @click="pause()"
v-if="player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="play()" v-else></button>
<button class="md-btn playback-button next" @click="next()"></button>
<button class="md-btn playback-button--small shuffle" @click="shuffle()"
v-if="player.currentMediaItem.shuffleMode == 0"></button>
<button class="md-btn playback-button--small shuffle active" @click="shuffle()"
v-else></button>
</div>
<div class="md-footer">
<div class="row volume-slider-container">
<div class="col-auto">
<div class="player-volume-glyph decrease"></div>
</div>
<div class="col">
<input type="range" class="web-slider volume-slider" max="1" min="0" step="0.01"
@input="setVolume($event.target.value)" :value="player.currentMediaItem.volume">
</div>
<div class="col-auto">
<div class="player-volume-glyph increase"></div>
</div>
</div>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'"
@click="showLyrics()"></button>
<button class="md-btn playback-button--small lyrics"
v-if="checkOrientation() == 'landscape'" @click="showLyricsInline()"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="screen = 'search'"></button>
</div>
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel search-panel" v-if="screen == 'search'">
<div class="search-header">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="screen = 'player'"></button>
</div>
<div class="col" style="display: flex;align-items: center;">
<div class="col">
<input type="text" placeholder="Artists, Songs, Lyrics, and More"
spellcheck="false" v-model="search.query" @change="searchQuery()"
v-on:keyup.enter="searchQuery()" class="search-input">
</div>
</div>
</div>
</div>
<div class="md-header search-type-container">
<button class="search-type-button" @click="search.searchType = 'applemusic';searchQuery()"
:class="searchTypeClass('applemusic')" style="width:100%;">Apple Music
</button>
<button class="search-type-button" @click="search.searchType = 'library';searchQuery()"
:class="searchTypeClass('library')" style="width:100%;">Library
</button>
</div>
<div class="md-header search-tab-container" v-if="search.state == 2">
<button class="search-tab" @click="search.tab = 'all'" :class="searchTabClass('all')">All
Results
</button>
<button class="search-tab" @click="search.tab = 'songs'"
:class="searchTabClass('songs')">Songs
</button>
<button class="search-tab" @click="search.tab = 'albums'"
:class="searchTabClass('albums')">Albums
</button>
<button class="search-tab" @click="search.tab = 'artists'"
:class="searchTabClass('artists')">Artists
</button>
</div>
</div>
<div class="search-body-container">
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 0">
<div
style="font-size: 17px;display:flex;flex-direction: column;justify-content: center;align-items: center;">
<img src="./assets/search.svg" style="width: 40px;margin: 32px;opacity: 0.85">
Search by song, album, artist, or lyrics.
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 1">
<!-- loading state -->
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" style="overflow-y:auto;" v-if="search.state == 2">
<template v-if="canShowSearchTab('songs')">
<div class="list-entry-header">Songs</div>
<div class="list-entry" v-for="song in search.results.songs"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.name }}
</div>
<div class="list-entry-artist">
{{ song.artistName }}
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="song in search.results['library-songs']"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.name }}
</div>
<div class="list-entry-artist">
{{ song.artistName }}
</div>
</div>
</div>
</div>
</template>
<template v-if="canShowSearchTab('albums')">
<div class="list-entry-header">Albums</div>
<div class="list-entry" v-for="album in search.results.albums">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.name }}
</div>
<div class="list-entry-artist">
{{ album.artistName }}
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="album in search.results['library-albums']">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.name }}
</div>
<div class="list-entry-artist">
{{ album.artistName }}
</div>
</div>
</div>
</div>
</template>
<template v-if="canShowSearchTab('artists')">
<div class="list-entry-header">Artists</div>
<div class="list-entry" v-for="artist in search.results.artists">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist" v-if="artist.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="artist in search.results['library-artists']">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist" v-if="artist.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
</template>
</div>
</transition>
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" style="overflow-y:auto;"
v-if="search.trackSelect">
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left"
@click="playMediaItemById(search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col-auto flex-center" v-if="search.selected.artwork"
style="display:flex;align-items: center;">
<div class="list-entry-image"
:style="{'--artwork': getAlbumArtUrlList(search.selected.artwork.url)}">
</div>
</div>
<div class="col flex-center" style="display:flex;align-items: center;">
<div style="width:100%;font-size: 18px;">
{{ search.selected.name }}
</div>
<div style="width:100%;font-size: 16px;">
{{ search.selected.artistName }}
</div>
<div style="width:100%;font-size: 14px;">
{{ parseTime(search.selected.durationInMillis) }}
</div>
</div>
</div>
</button>
</div>
<div class="md-body context-menu-body" style="height: auto;">
<button class="context-menu-item context-menu-item--left"
@click="playMediaItemById(search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play
</div>
<div class="col-auto">
▶️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left"
@click="playNext('song', search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play Next
</div>
<div class="col-auto">
⏭️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left"
@click="playLater('song', search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play Later
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="clearSelectedTrack()">Cancel</button>
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" v-if="player.songActions">
<div class="md-header">
</div>
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Add To Library
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Love
</div>
<div class="col-auto">
❤️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Share
</div>
<div class="col-auto">
🌐
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="player.songActions = false">Cancel</button>
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'queue'">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="screen = 'player'"></button>
</div>
<div class="col" style="display: flex;align-items: center;">
<div class="col">
Queue
</div>
</div>
<div class="col-auto">
<button
class="md-btn playback-button--small"
v-if="!player.currentMediaItem.autoplayEnabled"
@click="setAutoplay(true)"
>♾️</button>
<button
class="md-btn playback-button--small active"
v-else
@click="setAutoplay(false)"
>♾️</button>
</div>
</div>
</div>
<div class="md-body queue-body" v-if="!player.queue['_queueItems']">
Empty
</div>
<div class="md-body queue-body" style="overflow-y:auto;" id="list-queue" v-else>
<draggable
v-model="queue.temp"
handle=".handle"
filter=".passed"
@change="queueMove">
<template v-for="(song, position) in queue.temp">
<div class="list-entry" :class="getQueuePositionClass(position)">
<div class="row" style="width:100%;">
<div class="col-auto">
<div class="handle">
⬆️
<br>
⬇️
</div>
</div>
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.item.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.item.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.item.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.item.attributes.artistName }}
</div>
</div>
<div class="col-auto flex-center" style="text-align:right;">
<div v-if="position == player.queue['_position']">▶️</div>
</div>
</div>
</div>
</template>
</draggable>
</div>
<div class="md-footer">
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'lyrics'">
<div class="md-header">
<div class="list-entry">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active" @click="screen = 'player'"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="screen = 'search'"></button>
</div>
</div>
</transition>
</template>
<transition name="wpfade">
<div class="md-container md-container_panel connection-error-panel" v-if="connectedState != 1">
<div class="md-header">
</div>
<div class="md-body" style="display:flex;justify-content: center;align-items: center;">
<div v-if="connectedState == 0">
Loading...
</div>
<div v-else>
<h3 style="text-align:center;">Connection Interrupted</h3>
<button class="md-btn md-btn-primary"
style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;"
@click="connect()">Retry
</button>
</div>
</div>
<div class="md-footer">
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="false">
<div class="md-header">
</div>
<div class="md-body">
</div>
<div class="md-footer">
</div>
</div>
</transition>
</div>
<script src="index.js?v=1"></script>
</body>
</html>

View file

@ -0,0 +1,500 @@
var socket;
// vue instance
var app = new Vue({
el: '#app',
data: {
screen: "player",
player: {
currentMediaItem: {},
songActions: false,
lyrics: {},
lyricsMediaItem: {},
lyricsDebug: {
current: 0,
start: 0,
end: 0
},
queue: {},
lowerPanelState: "controls",
userInteraction: false
},
queue: {
temp: []
},
search: {
query: "",
results: [],
state: 0,
tab: "all",
searchType: "applemusic",
trackSelect: false,
selected: {},
queue: {},
},
connectedState: 0,
url: window.location.hostname,
// url: "localhost",
},
methods: {
resetPlayerUI() {
this.player.lowerPanelState = "controls";
},
musicAppVariant() {
if (navigator.userAgent.match(/Android/i)) {
return "Apple Music";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "Music";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'Music';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'Apple Music Electron';
} else {
return 'Apple Music Electron';
}
}
},
checkOrientation() {
// check orientation of device
if (window.innerHeight > window.innerWidth) {
return 'portrait'
} else {
return 'landscape';
}
},
checkPlatformMD() {
// check if platfom is desktop or mobile
if (navigator.userAgent.match(/Android/i)) {
return "mobile";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "mobile";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'desktop';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'desktop';
} else {
return 'desktop';
}
}
},
checkPlatform() {
if (navigator.userAgent.match(/Android/i)) {
return "android";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "ios";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'mac';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'win';
} else {
return 'linux';
}
}
},
artworkPlaying() {
if (this.player.currentMediaItem.status) {
return
} else {
return ["paused"]
}
},
setAutoplay(value) {
socket.send(JSON.stringify({
"action": "set-autoplay",
"autoplay": value
}));
this.getCurrentMediaItem()
if (value) {
setTimeout(() => {
this.getQueue()
}, 1000)
}else{
this.getQueue()
}
},
seekTo(time, adjust = true) {
if (adjust) {
time = parseInt(time / 1000)
}
socket.send(JSON.stringify({
action: "seek",
time: time
}));
},
setVolume(volume) {
socket.send(JSON.stringify({
action: "volume",
volume: volume
}));
},
getQueue() {
socket.send(JSON.stringify({
action: "get-queue"
}))
},
play() {
socket.send(JSON.stringify({
action: "play"
}))
},
pause() {
socket.send(JSON.stringify({
action: "pause"
}))
},
next() {
socket.send(JSON.stringify({
action: "next"
}))
},
previous() {
socket.send(JSON.stringify({
action: "previous"
}))
},
searchArtist() {
this.search.query = this.player.currentMediaItem.artistName;
this.screen = "search";
this.searchQuery();
},
trackSelect(song) {
this.search.selected = song;
this.search.trackSelect = true
},
clearSelectedTrack() {
this.search.selected = {}
this.search.trackSelect = false
},
getArtworkColor(hex) {
return `#${hex}`
},
playMediaItemById(id) {
socket.send(JSON.stringify({
action: "play-mediaitem",
id: id
}))
this.screen = "player";
},
playNext(type, id) {
socket.send(JSON.stringify({
action: "play-next",
type: type,
id: id
}))
},
playLater(type, id) {
socket.send(JSON.stringify({
action: "play-later",
type: type,
id: id
}))
},
searchQuery() {
if (this.search.query.length == 0) {
this.search.state = 0;
return;
}
this.search.state = 1;
var actionType = "search"
if (this.search.searchType == "library") {
actionType = "library-search"
}
socket.send(JSON.stringify({
"action": actionType,
"term": this.search.query,
"limit": 20
}))
},
quickSearch() {
var search = prompt("Search for a song", "")
if (search == null || search == "") {
return
}
socket.send(JSON.stringify({
action: "quick-play",
term: search
}))
},
parseTime(value) {
var minutes = Math.floor(value / 60000);
var seconds = ((value % 60000) / 1000).toFixed(0);
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
},
parseTimeDecimal(value) {
var minutes = Math.floor(value / 60000);
var seconds = ((value % 60000) / 1000).toFixed(0);
return minutes + "." + (seconds < 10 ? '0' : '') + seconds;
},
hmsToSecondsOnly(str) {
var p = str.split(':'),
s = 0,
m = 1;
while (p.length > 0) {
s += m * parseInt(p.pop(), 10);
m *= 60;
}
return s;
},
getCurrentTime() {
return parseFloat(this.hmsToSecondsOnly(this.parseTime(this.player.currentMediaItem.durationInMillis - this.player.currentMediaItem.remainingTime)));
},
percentage(partial, full) {
return (100 * partial) / full
},
getLyricBGStyle(start, end) {
var currentTime = this.getCurrentTime();
var duration = this.player.currentMediaItem.durationInMillis
var start2 = this.hmsToSecondsOnly(start)
var end2 = this.hmsToSecondsOnly(end)
var currentProgress = ((100 * (currentTime)) / (end2))
// check if currenttime is between start and end
this.player.lyricsDebug.start = start2
this.player.lyricsDebug.end = end2
this.player.lyricsDebug.current = currentTime
if (currentTime >= start2 && currentTime <= end2) {
return {
"--bgSpeed": `${(end2 - start2)}s`
}
} else {
return {}
}
},
getLyricClass(start, end) {
var currentTime = this.getCurrentTime();
// check if currenttime is between start and end
if (currentTime >= start && currentTime <= end) {
setTimeout(() => {
if (document.querySelector(".lyric-line.active")) {
document.querySelector(".lyric-line.active").scrollIntoView({
behavior: "smooth",
block: "center"
})
}
}, 200)
return "active"
} else {
return ""
}
},
getAlbumArtUrl(size = 600) {
if (this.player.currentMediaItem.artwork) {
return `url("${this.player.currentMediaItem.artwork.url.replace('{w}', size).replace('{h}', size)}")`;
} else {
return "";
}
},
getAlbumArtUrlList(url, size = 64) {
return `url("${url.replace('{w}', size).replace('{h}', size)}")`;
},
searchTabClass(tab) {
if (tab == this.search.tab) {
return "active";
}
},
searchTypeClass(type) {
if (type == this.search.searchType) {
return "active";
}
},
getQueuePositionClass(position) {
if (this.player.queue["_position"] == position) {
return ["playing", "passed"]
} else if (this.player.queue["_position"] > position) {
return ["passed"]
}
},
showQueue() {
this.queue.temp = this.player["queue"]["_queueItems"]
this.screen = "queue"
this.getQueue()
},
queueMove(evt) {
console.log(evt)
console.log(`new: ${evt.moved.newIndex} old: ${evt.moved.oldIndex}`)
this.queue.temp.splice(evt.moved.newIndex, 0, this.queue.temp.splice(evt.moved.oldIndex, 1)[0])
socket.send(JSON.stringify({
action: "queue-move",
from: evt.moved.oldIndex,
to: evt.moved.newIndex
}))
this.getQueue()
return true
},
repeat() {
socket.send(JSON.stringify({
action: "repeat"
}))
this.getCurrentMediaItem()
},
shuffle() {
socket.send(JSON.stringify({
action: "shuffle"
}))
this.getCurrentMediaItem()
},
getLyrics() {
socket.send(JSON.stringify({
action: "get-lyrics",
}))
},
showLyrics() {
this.getLyrics()
this.screen = "lyrics"
},
showLyricsInline() {
this.getLyrics()
this.player.lowerPanelState = "lyrics"
},
parseLyrics() {
var xml = this.stringToXml(this.player.lyricsMediaItem.ttml)
var json = xmlToJson(xml);
this.player.lyrics = json
},
stringToXml(st) {
// string to xml
var xml = (new DOMParser()).parseFromString(st, "text/xml");
return xml;
},
canShowSearchTab(tab) {
if (tab == this.search.tab || this.search.tab == "all") {
return true;
} else {
return false;
}
},
getCurrentMediaItem() {
socket.send(JSON.stringify({
action: "get-currentmediaitem"
}))
},
connect() {
let self = this;
this.connectedState = 0;
if (this.url === "") {
this.url = prompt("Host IP", "localhost")
}
socket = new WebSocket(`ws://${this.url}:26369`);
socket.onopen = (e) => {
console.log(e);
console.log('connected');
app.connectedState = 1;
self.screen = "player"
self.clearSelectedTrack()
}
socket.onclose = (e) => {
console.log(e);
console.log('disconnected');
app.connectedState = 2;
}
socket.onerror = (e) => {
console.log(e);
console.log('error');
app.connectedState = 2;
}
socket.onmessage = (e) => {
const response = JSON.parse(e.data);
switch (response.type) {
default:
break;
case "queue":
self.player.queue = response.data;
self.queue.temp = response.data["_queueItems"];
self.$forceUpdate()
if (self.screen == "queue") {
setTimeout(() => {
document.querySelector(".playing").scrollIntoView({
behavior: "smooth",
block: "start"
})
}, 200)
}
break;
case "lyrics":
self.player.lyrics = response.data;
self.$forceUpdate()
break;
case "searchResultsLibrary":
self.search.results = response.data;
self.search.state = 2;
break;
case "searchResults":
self.search.results = response.data;
self.search.state = 2;
break;
case "playbackStateUpdate":
if (!self.player.userInteraction) {
self.updatePlaybackState(response.data)
}
break;
}
// console.log(e.data);
}
},
updatePlaybackState(mediaitem) {
var lyricsDisplayed = this.screen == "lyrics" || this.player.lowerPanelState == "lyrics"
if (this.player.currentMediaItem["isrc"] != mediaitem["isrc"]) {
if (lyricsDisplayed) {
this.getLyrics()
}
if (this.screen == "queue") {
this.getQueue()
}
}
this.player.currentMediaItem = mediaitem
}
},
});
function xmlToJson(xml) {
// Create the return object
var obj = {};
if (xml.nodeType == 1) { // element
// do attributes
if (xml.attributes.length > 0) {
obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
}
}
} else if (xml.nodeType == 3) { // text
obj = xml.nodeValue;
}
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof (obj[nodeName]) == "undefined") {
obj[nodeName] = xmlToJson(item);
} else {
if (typeof (obj[nodeName].push) == "undefined") {
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xmlToJson(item));
}
}
}
return obj;
};
window.onresize = function () {
app.resetPlayerUI()
}
app.connect()

View file

@ -0,0 +1,32 @@
{
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone",
"scope": "/",
"start_url": "/",
"name": "AME Remote",
"short_name": "AME Remote",
"description": "Apple Music Electron Remote",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View file

@ -0,0 +1,20 @@
{
"name": "AME",
"short_name": "AME",
"description": "AME",
"icons": [
{
"src": "images/icon.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icon.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "fullscreen",
"start_url": "/web-remote/index.html",
"orientation": "portrait"
}

2
resources/web-remote/sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,759 @@
@import url("ameframework.css");
:root {
--appleEase: cubic-bezier(0.42, 0, 0.58, 1);
--keyColor: #fa586a;
--keyColor-rgb: 250, 88, 106;
--keyColor-rollover: #ff8a9c;
--keyColor-rollover-rgb: 255, 138, 156;
--keyColor-pressed: #ff7183;
--keyColor-pressed-rgb: 255, 113, 131;
--keyColor-deepPressed: #ff8a9c;
--keyColor-deepPressed-rgb: 255, 138, 156;
--keyColor-disabled: rgba(250, 88, 106, 0.35);
}
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
/* Modern style overlay scrollbars */
::-webkit-scrollbar {
width: 16px;
height: 24px;
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-track-piece {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: transparent;
border: 6px solid transparent;
box-shadow: inset 0px 0px 10px 10px rgb(200 200 200 / 50%);
border-radius: 16px;
min-height: 64px;
transition: border 1s;
}
::-webkit-scrollbar-thumb:hover {
border: 5px solid transparent;
box-shadow: inset 0px 0px 10px 10px rgb(200 200 200 / 80%);
}
#app {
--artwork: url();
max-width: 600px;
width: 100%;
height: 100%;
background: rgb(20 20 20);
color: white;
user-select: none;
margin: 0 auto;
position: relative;
overflow: hidden;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
#app:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: var(--artwork);
background-position: center;
background-size: cover;
opacity: 0.25;
filter: blur(32px) saturate(180%);
}
body {
background: #111;
font-family: "Segoe UI Variable Display", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
input[type="range"].web-slider {
-webkit-appearance: none;
height: 4px;
background: rgba(255, 255, 255, 0.6);
border-radius: 5px;
background-size: 70% 100%;
background-repeat: no-repeat;
}
input[type="range"].web-slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: rgb(255 255 255);
cursor: ew-resize;
box-shadow: 0 0 2px 0 #555;
}
input[type=range].web-slider::-webkit-slider-runnable-track {
-webkit-appearance: none;
box-shadow: none;
border: none;
background: transparent;
}
.nopadding {
padding: 0px;
}
.md-btn {
font-family: inherit;
}
.player-duration-time {
opacity: 0.5;
}
.player-artwork-container {
display: flex;
align-items: center;
justify-content: center;
}
.player-duration-container {
font-size: 0.85em;
font-weight: 500;
}
.media-artwork {
--artwork: url("");
width: 80vw;
height: 80vw;
max-height: 500px;
max-width: 500px;
background: black;
background-image: var(--artwork);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 8px;
box-shadow: inset 0px 0px 0px 1px rgb(200 200 200 / 16%), 0 8px 40px rgb(0 0 0 / 0.55);
transition: transform .10s var(--appleEase);
}
.media-artwork.paused {
transition: transform .35s var(--appleEase);
transform: scale(0.85);
}
.playback-slider {
width: 90%;
}
.volume-slider {
width: 100%;
}
.volume-slider-container {
width: 90%;
margin: 0 auto;
padding: 0px;
}
.volume-slider-container .col-auto,
.volume-slider-container .col {
display: flex;
align-items: center;
justify-self: center;
padding: 0px;
margin: 0px;
}
.playback-button {
font-size: 2em;
width: 64px;
height: 64px;
padding: 0px;
background: transparent;
border: 0px;
border-radius: 0px;
box-shadow: unset;
background-size: 24px;
background-position: center;
background-repeat: no-repeat;
}
.playback-button:active {
transform: scale(0.95);
}
.playback-button--small {
font-size: 1em;
color: inherit;
background-size: 18px;
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
width: 64px;
height: 32px;
border: 0px;
box-shadow: unset;
}
.playback-button--small.active {
background-color: rgb(200 200 200 / 10%);
}
.playback-button--small.search {
background-image: url("./assets/search.svg");
}
.playback-button--small.queue {
background-image: url("./assets/list.svg");
}
.playback-button--small.lyrics {
background-image: url("./assets/quote-right.svg");
}
.playback-button--small.shuffle {
background-image: url("./assets/shuffle.svg");
}
.playback-button--small.repeat {
background-image: url("./assets/repeat.svg");
}
.playback-button--small.repeat.repeatOne {
background-color: var(--keyColor);
}
.playback-button.pause {
background-image: url('./assets/pause.svg');
}
.playback-button.play {
background-image: url('./assets/play.svg');
}
.playback-button.next {
background-image: url('./assets/forward.svg');
}
.playback-button.previous {
background-image: url('./assets/backward.svg');
}
.playback-buttons {
display: flex;
align-items: center;
justify-content: center;
}
.player-volume-glyph {
width: 32px;
height: 16px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
.player-volume-glyph.decrease {
background-image: url("./assets/volume-down.svg");
opacity: 0.5;
}
.player-volume-glyph.increase {
background-image: url("./assets/volume-up.svg");
opacity: 0.5;
}
.player-track-info {
width: 90%;
margin: 0 auto;
}
.player-song-title {
font-size: 1.25em;
text-align: left;
margin: 0 auto;
font-weight: 500;
}
.player-song-artist {
font-size: 1.0em;
text-align: left;
margin: 0 auto;
color: var(--keyColor);
font-weight: 400;
}
.player-song-artist:hover {
cursor: pointer;
text-decoration: underline;
}
.player-more-container {
display: flex;
align-items: center;
justify-content: center;
}
.player-more-button {
appearance: none;
width: 32px;
height: 32px;
border-radius: 50%;
border: 0px;
background: var(--keyColor);
cursor: pointer;
box-shadow: inset 0px 0px 0px 1px rgb(200 200 200 / 16%);
color: white;
font-weight: bold;
padding: 0px;
font-size: 16px;
}
.back-button {
width: 40px;
height: 40px;
background-color: transparent;
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
background-image: url("./assets/arrow-left.svg");
border: 0px;
border-radius: 0px;
}
.header-text {
height: 40px;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
padding: 12px;
font-size: 1em;
font-family: inherit;
border-radius: 8px;
border: 0px;
background: rgb(20 20 20 / 0.85);
color: #eee;
}
.flex-center {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.list-entry-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
font-size: 1em;
font-family: inherit;
}
.list-entry {
display: flex;
align-items: center;
/* justify-content: space-between; */
padding: 12px;
font-size: 1em;
font-family: inherit;
border-bottom: 1px solid rgba(255 255 255 / 0.1);
cursor: pointer;
}
.list-entry-image {
--artwork: url("");
width: 64px;
height: 64px;
background: var(--artwork);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 8px;
box-shadow: inset 0px 0px 0px 1px rgb(200 200 200 / 16%), 0 8px 40px rgb(0 0 0 / 0.55);
}
.list-entry-image.artist {
border-radius: 50%;
}
.list-entry-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
margin-left: 12px;
}
.list-entry-name {
font-size: 14px;
font-weight: 500;
overflow: hidden;
width: 100%;
}
.list-entry-artist {
font-size: 12px;
overflow: hidden;
width: 100%;
}
.list-entry .handle {
height: 100%;
width: 28px;
background:var(--keyColor);
display: flex;
justify-content: center;
align-items: center;
}
.md-container {
width: 100%;
position: relative;
}
.search-panel {
background: rgb(0 0 0 / 50%);
}
.search-header {
position: absolute;
width: 100%;
z-index: 1;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid rgb(200 200 200 / 8%);
}
.connection-error-panel {
background: rgb(0 0 0 / 50%);
}
.search-type-container {
display: flex;
}
.search-type-button {
background: rgb(20 20 20 / 0.85);
border-radius: 50px;
color: white;
border: 0px;
box-shadow: unset;
font-family: inherit;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
margin: 8px;
margin-top: 0px;
margin-bottom: 0px;
}
.search-type-button.active {
background: var(--keyColor);
}
.search-tab-container {
overflow: auto;
white-space: nowrap;
overflow-y: hidden;
}
.search-body-container {
position: relative;
width: 100%;
height: 100%;
}
.queue-body {
width: 100%;
height: 100%;
}
.search-body {
position: absolute;
width: 100%;
height: 100%;
padding-top: 220px;
}
.search-tab {
background: rgb(20 20 20 / 0.85);
border-radius: 50px;
color: white;
border: 0px;
box-shadow: unset;
font-family: inherit;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
}
.search-tab.active {
background: var(--keyColor);
}
.context-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.context-menu .context-menu-item {
--borderRadius: 10px;
appearance: none;
width: 100%;
display: block;
font-size: 1.10em;
padding: 18px 20px;
font-family: inherit;
border: 0px;
font-weight: 600;
border-radius: 0px;
border-bottom: 1px solid rgb(200 200 200 / 15%);
margin: 0px;
background: rgb(60 60 60 / 80%);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
color: #ccc;
}
.context-menu .context-menu-item:hover {
cursor: pointer;
}
.context-menu .context-menu-item:active {
filter: brightness(75%);
}
.context-menu .context-menu-item:first-child {
border-radius: 0px;
border-top-left-radius: var(--borderRadius);
border-top-right-radius: var(--borderRadius);
}
.context-menu .context-menu-item:last-child {
border-radius: 0px;
border-bottom-left-radius: var(--borderRadius);
border-bottom-right-radius: var(--borderRadius);
border-bottom: 0px;
}
.context-menu .context-menu-item:only-child {
border-radius: var(--borderRadius);
}
.context-menu .context-menu-item.context-menu-item--left {
text-align: left;
}
.context-menu .context-menu-body {
display: flex;
align-items: flex-end;
justify-content: flex-end;
flex-direction: column;
}
.lyric-body {
-webkit-mask-image: -webkit-gradient(linear, left 95%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
overflow-y: scroll;
overflow-x: hidden;
display:flex;
flex-flow: column;
}
.lyric-line {
--bgSpeed: 1s;
appearance: none;
color: white;
font-size: 2rem;
transform: scale(0.8);
transform-origin: left center;
transition: transform 0.2s var(--appleEase);
opacity: 0.75;
width: auto;
display: inline-block;
margin: 10px;
margin-left: 5%;
margin-right: 0px;
}
.lyric-line:hover {
cursor: pointer;
}
.lyric-line:hover::after {
content: ' ';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
transform: scale(1.06);
background: rgba(200, 200, 200, 0.1);
pointer-events: none;
border-radius: 10px;
-webkit-backface-visibility: hidden;
}
.lyric-line.active {
--bgSpeed: 1s;
opacity: 1;
transform: scale(1);
/*background: var(--keyColor);*/
transition: transform 0.2s var(--appleEase);
}
.lyric-line:not(.active) {
filter: blur(1px)
}
.lyricWaiting {
margin-top: 8px;
display: none;
}
.lyric-line.active .lyricWaiting {
display: inline-flex;
animation: lyricWaitingLine 6s cubic-bezier(0.42, 0, 0.58, 1) infinite;
}
.lyric-line.active .lyricWaiting > div {
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
margin: 3px;
}
@keyframes lyricWaitingLine {
0% {
opacity: 0;
transform: scale(0.85);
}
50% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.85);
}
}
.lyric-line2:before {
background: var(--keyColor);
content: '';
width: 0%;
height: 6px;
position: absolute;
bottom: -8px;
left: 0;
border-radius: 10px;
z-index: -1;
transition: width var(--bgSpeed);
}
.lyric-line2.active:before {
width: 100%;
transition: width var(--bgSpeed);
}
.player_top {
height: 100%;
}
/* Small Screen */
@media only screen and (max-height: 668px) {
#app {
zoom: 0.8;
}
}
/* Big Screen */
@media only screen and (orientation: landscape) {
#app {
max-width: 100%;
}
.player-panel {
display: flex;
flex-direction: row;
}
.player_top {
width: 100%;
}
.player_bottom {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
margin: 0 auto;
}
.media-artwork {
width: 45vw;
height: 45vw;
margin: 60px;
}
.player-song-title {
font-size: 2em;
}
.player-song-artist {
font-size: 1.25em;
}
.search-panel {
}
.md-footer {
width: 100%;
}
}
/* Transitions */
.wpfade-enter-active,
.wpfade-leave-active {
transition: opacity .1s var(--appleEase);
}
.wpfade-enter,
.wpfade-leave-to {
opacity: 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long