Added the music bot GUI
parent
7f1bc4db5f
commit
e8c3c0a004
|
@ -1,4 +1,7 @@
|
|||
# Changelog:
|
||||
* **02.02.20**
|
||||
- Added a music bot GUI
|
||||
|
||||
* **30.01.20**
|
||||
- Improved chat message parsing
|
||||
- Fixed copy & paste error
|
||||
|
|
|
@ -45,6 +45,8 @@ $bot_thumbnail_height: 9em;
|
|||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 3.25em;
|
||||
|
||||
.block, .button {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -191,6 +193,35 @@ $bot_thumbnail_height: 9em;
|
|||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.mode-channel_chat) {
|
||||
.mode-channel_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-private_chat) {
|
||||
.mode-private_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-client_info) {
|
||||
.mode-client_info { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-music_bot) {
|
||||
.mode-music_bot { display: none; }
|
||||
}
|
||||
|
||||
&.mode-music_bot {
|
||||
.mode-music_bot {
|
||||
&.right {
|
||||
margin-left: 8.5em;
|
||||
}
|
||||
&.left {
|
||||
margin-right: 8.5em;
|
||||
}
|
||||
|
||||
width: 60em; /* same width so flex-shrik applies equaly */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -864,6 +895,13 @@ $bot_thumbnail_height: 9em;
|
|||
line-height: 1.1em;
|
||||
|
||||
word-wrap: break-word;
|
||||
|
||||
.htmltag-client, .htmltag-channel {
|
||||
display: inline;
|
||||
|
||||
font-weight: bold;
|
||||
color: $color_client_normal;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
@ -1431,6 +1469,405 @@ $bot_thumbnail_height: 9em;
|
|||
height: $bot_thumbnail_height;
|
||||
|
||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-song-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
min-width: 1em;
|
||||
|
||||
.song-name {
|
||||
font-size: 1.5em;
|
||||
|
||||
min-width: 1em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
align-self: center;
|
||||
color: #999999;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.song-description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container-timeline {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
margin-bottom: .5em;
|
||||
|
||||
.timestamps {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
color: #999;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
$timeline_height: .6em;
|
||||
.timeline {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1em;
|
||||
height: $timeline_height;
|
||||
cursor: pointer;
|
||||
background-color: #242527;
|
||||
border-radius: 0.2em;
|
||||
overflow: visible;
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
border-radius: .2em;
|
||||
}
|
||||
|
||||
.indicator-buffered {
|
||||
background-color: #2f3133;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.indicator-playtime {
|
||||
background-color: #4370a2;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
$thumb_width: 1.2em;
|
||||
$thumb_inner_width: 0.4em;
|
||||
.thumb {
|
||||
position: absolute;
|
||||
|
||||
height: $thumb_width;
|
||||
width: $thumb_width;
|
||||
|
||||
left: -($thumb_width / 2);
|
||||
bottom: -$thumb_width / 2 + $timeline_height / 2;
|
||||
|
||||
background-color: #a5a5a5;
|
||||
box-shadow: 0 0 0.5em 1px #a5a5a53d;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.dot {
|
||||
align-self: center;
|
||||
|
||||
height: $thumb_inner_width;
|
||||
width: $thumb_inner_width;
|
||||
|
||||
|
||||
background-color: #4370a2;
|
||||
box-shadow: 0 0 0.1em 1px hsla(212, 41%, 60%, 1);
|
||||
|
||||
border-radius: 50%;
|
||||
}
|
||||
border-radius: 50%;
|
||||
|
||||
//@include transition(.4s);
|
||||
|
||||
margin-left: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
.button {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
margin-right: .5em;
|
||||
margin-left: .5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
|
||||
fill: #242527;
|
||||
@include transition($button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #0a0a0a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-play, .button-pause {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-playlist {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: calc(3em + 4px);
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
margin-bottom: 5px;
|
||||
margin-top: 1em;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background: #2b2b28;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 0.2em;
|
||||
border: 1px #161616 solid;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
|
||||
font-size: 1.5em;
|
||||
color: hsla(0, 1%, 40%, 1);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 8em;
|
||||
font-size: .8em;
|
||||
align-self: center;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 3em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: 0.2em;
|
||||
background-color: rgba(43, 43, 40, 1);
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.entry {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
color: #999;
|
||||
border-bottom: 1px solid #242527;
|
||||
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(220, 0%, 20%, 1);
|
||||
}
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
&.animation {
|
||||
@include transition(opacity 0.5s ease-in-out, height 0.5s ease-in);
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
@include transition(opacity 0.5s ease-in-out, height 0.5s ease-in, padding 0.5s ease-in);
|
||||
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&.reordering {
|
||||
z-index: 10000;
|
||||
|
||||
position: fixed;
|
||||
cursor: move;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: 0.2em;
|
||||
background-color: #2b2b28;
|
||||
}
|
||||
|
||||
.container-thumbnail {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
align-self: center;
|
||||
|
||||
height: .9em;
|
||||
width: 1.6em;
|
||||
|
||||
font-size: 3em;
|
||||
position: relative;
|
||||
|
||||
border-radius: 0.05em;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.container-data {
|
||||
margin-left: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
min-width: 2em;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
&.second {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.container-delete {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
margin-left: .5em;
|
||||
|
||||
opacity: .4;
|
||||
@include transition($button_hover_animation_time ease-in-out);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.length {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.current-song {
|
||||
background-color: hsla(130, 50%, 30%, .25);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(130, 50%, 50%, .25);
|
||||
}
|
||||
|
||||
.container-delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reorder-indicator {
|
||||
$indicator_thickness: .2em;
|
||||
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: $indicator_thickness solid hsla(0, 0%, 30%, 1);
|
||||
|
||||
margin-top: $indicator_thickness / -2;
|
||||
margin-bottom: $indicator_thickness / -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ $animation_length: .5s;
|
|||
height: 80%; /* "default" settings */
|
||||
width: 100%;
|
||||
|
||||
min-height: 25em;
|
||||
min-height: 27em; /* fits with the music bot interface */
|
||||
min-width: 100px;
|
||||
|
||||
display: flex;
|
||||
|
|
|
@ -510,18 +510,161 @@
|
|||
<div class="player">
|
||||
<div class="container-thumbnail">
|
||||
<div class="thumbnail">
|
||||
<!-- https://i.ytimg.com/vi/DeXoACwOT1o/maxresdefault.jpg -->
|
||||
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
|
||||
<img src="https://i.ytimg.com/vi/DeXoACwOT1o/maxresdefault.jpg">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-song-info">
|
||||
<a class="song-name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</a>
|
||||
<a class="song-description"></a>
|
||||
</div>
|
||||
<div class="container-timeline">
|
||||
<div class="timestamps">
|
||||
<div class="current">00:01:13</div>
|
||||
<div class="max">00:03:22</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="indicator indicator-buffered"></div>
|
||||
<div class="indicator indicator-playtime"></div>
|
||||
<div class="thumb"><div class="dot"></div></div>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<div class="button button-rewind">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path transform="rotate(180, 256, 256)" d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
|
||||
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
|
||||
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
|
||||
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
|
||||
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-play">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path d="M500.203,236.907L30.869,2.24c-6.613-3.285-14.443-2.944-20.736,0.939C3.84,7.083,0,13.931,0,21.333v469.333
|
||||
c0,7.403,3.84,14.251,10.133,18.155c3.413,2.112,7.296,3.179,11.2,3.179c3.264,0,6.528-0.747,9.536-2.24l469.333-234.667
|
||||
C507.435,271.467,512,264.085,512,256S507.435,240.533,500.203,236.907z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-pause">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path transform='rotate(90, 256, 256)' d="M85.333,213.333h341.333C473.728,213.333,512,175.061,512,128s-38.272-85.333-85.333-85.333H85.333
|
||||
C38.272,42.667,0,80.939,0,128S38.272,213.333,85.333,213.333z"/>
|
||||
<path transform='rotate(90, 256, 256)' d="M426.667,298.667H85.333C38.272,298.667,0,336.939,0,384s38.272,85.333,85.333,85.333h341.333
|
||||
C473.728,469.333,512,431.061,512,384S473.728,298.667,426.667,298.667z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-forward">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
|
||||
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
|
||||
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
|
||||
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
|
||||
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-playlist">
|
||||
<div class="overlay overlay-loading">
|
||||
<a>{{tr "Fetching playlist..." /}}</a>
|
||||
</div>
|
||||
<div class="overlay overlay-no-permissions">
|
||||
<a>{{tr "You don't have permissions to see this playlist" /}}</a>
|
||||
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
<div class="overlay overlay-error">
|
||||
<a>{{tr "An error occurred while fetching the playlist" /}}</a>
|
||||
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
<div class="overlay overlay-empty">
|
||||
<a>{{tr "The playlist is currently empty." /}}</a>
|
||||
<button class="btn btn-green button-song-add">{{tr "Add a song" /}}</button>
|
||||
</div>
|
||||
<div class="playlist">
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
|
||||
<img src="https://i.ytimg.com/vi/KaXXVzGy7Y8/maxresdefault.jpg">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">2-Hours Epic Music | THE POWER OF EPIC MUSIC - Best Of Collection - Vol.5 - 2019</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">It is time for another 2-Hour Epic Music Mix. So far 2019 has been one amazing year for the orchestral epic music genre and i cannot wait for more. Also incl...</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">This is an example song description which needs some work</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">This is an example song description which needs some work</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="client-name"></a>
|
||||
<div class="container-description">
|
||||
<a class="client-description">error: description</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-close"></div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_music_playlist_entry" type="text/html">
|
||||
<div class="entry shown">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">error: name</div>
|
||||
<div class="container-delete button-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">error: description</div>
|
||||
<div class="length">--:--:--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_select_info" type="text/html">
|
||||
<div class="select_info" style="width: 100%; max-width: 100%">
|
||||
<button type="button" class="close button-modal-close" aria-label="Close">
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="forward_2_">
|
||||
<path d="M35.518,24.306l-10.971,8.592C24.329,33.065,24,32.978,24,32.703v-7.971v-0.022l-10.453,8.187
|
||||
C13.329,33.065,13,32.978,13,32.703V15.28c0-0.275,0.329-0.362,0.547-0.194L24,23.242V23.22V15.28c0-0.275,0.329-0.362,0.547-0.194
|
||||
l11.033,8.608C35.798,23.862,35.734,24.138,35.518,24.306z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 669 B |
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<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 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path transform='rotate(90, 256, 256)' d="M85.333,213.333h341.333C473.728,213.333,512,175.061,512,128s-38.272-85.333-85.333-85.333H85.333
|
||||
C38.272,42.667,0,80.939,0,128S38.272,213.333,85.333,213.333z"/>
|
||||
<path transform='rotate(90, 256, 256)' d="M426.667,298.667H85.333C38.272,298.667,0,336.939,0,384s38.272,85.333,85.333,85.333h341.333
|
||||
C473.728,469.333,512,431.061,512,384S473.728,298.667,426.667,298.667z"/>
|
||||
</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>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1021 B |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="300px" width="300px" version="1.0" viewBox="-300 -300 600 600" xml:space="preserve">
|
||||
<circle stroke="#AAA" stroke-width="10" r="280" fill="#FFF"/>
|
||||
<text style="letter-spacing:1;text-anchor:middle;text-align:center;stroke-opacity:.5;stroke:#000;stroke-width:2;fill:#444;font-size:360px;font-family:Bitstream Vera Sans,Liberation Sans, Arial, sans-serif;line-height:125%;writing-mode:lr-tb;" transform="scale(.2)">
|
||||
<tspan y="-40" x="8">NO IMAGE</tspan>
|
||||
<tspan y="400" x="8">AVAILABLE</tspan>
|
||||
</text>
|
||||
</svg>
|
Before Width: | Height: | Size: 574 B |
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="playlist_1_">
|
||||
<path d="M37.192,23.032c-0.847,0.339-0.179-0.339-0.179-0.339s1.422-2.092,0.406-3.786c-0.793-1.321-3.338-1.075-4.42-1.669v14.154
|
||||
c0,0.037-0.016,0.07-0.022,0.106c-0.154,1.504-1.607,3.034-3.696,3.712c-2.559,0.829-5.102,0.063-5.678-1.711
|
||||
c-0.574-1.774,1.034-3.887,3.595-4.717c0.66-0.189,2.207-0.439,2.801-0.193V12.607C30,12.273,30.271,12,30.607,12h1.785
|
||||
C32.728,12,33,12.273,33,12.607v0.549c1.542,1.004,6.18,1.455,6.851,4.139C40.656,20.52,38.038,22.693,37.192,23.032z M12.5,20H28
|
||||
v-3H12.5c-0.275,0-0.5,0.225-0.5,0.5v2C12,19.775,12.225,20,12.5,20z M12.5,26H28v-3H12.5c-0.275,0-0.5,0.225-0.5,0.5v2
|
||||
C12,25.775,12.225,26,12.5,26z M22.625,29H12.5c-0.275,0-0.5,0.225-0.5,0.5v2c0,0.275,0.225,0.5,0.5,0.5h8.551
|
||||
C21.227,30.925,21.779,29.887,22.625,29z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="rewind_2_">
|
||||
<path d="M35,15.28v17.423c0,0.274-0.329,0.362-0.547,0.194L24,24.711v0.022v7.971c0,0.274-0.329,0.362-0.547,0.194l-10.971-8.592
|
||||
c-0.217-0.168-0.28-0.443-0.062-0.611l11.033-8.608C23.671,14.918,24,15.005,24,15.28v7.939v0.023l10.453-8.156
|
||||
C34.671,14.918,35,15.005,35,15.28z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 653 B |
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace connection {
|
||||
import Conversation = chat.channel.Conversation;
|
||||
import MusicInfo = chat.MusicInfo;
|
||||
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
|
@ -54,6 +55,14 @@ namespace connection {
|
|||
|
||||
this["notifyconversationhistory"] = this.handleNotifyConversationHistory;
|
||||
this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete;
|
||||
|
||||
this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate;
|
||||
this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange;
|
||||
|
||||
this["notifyplaylistsongadd"] = this.handleNotifyPlaylistSongAdd;
|
||||
this["notifyplaylistsongremove"] = this.handleNotifyPlaylistSongRemove;
|
||||
this["notifyplaylistsongreorder"] = this.handleNotifyPlaylistSongReorder;
|
||||
this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded;
|
||||
}
|
||||
|
||||
private loggable_invoker(unique_id, client_id, name) : log.server.base.Client | undefined {
|
||||
|
@ -1037,5 +1046,115 @@ namespace connection {
|
|||
conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"]));
|
||||
}
|
||||
}
|
||||
|
||||
handleNotifyMusicStatusUpdate(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const bot_id = parseInt(json["bot_id"]);
|
||||
const client = this.connection.client.channelTree.find_client_by_dbid(bot_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id);
|
||||
return;
|
||||
}
|
||||
|
||||
client.events.fire("music_status_update", {
|
||||
player_replay_index: parseInt(json["player_replay_index"]),
|
||||
player_buffered_index: parseInt(json["player_buffered_index"])
|
||||
});
|
||||
}
|
||||
|
||||
handleMusicPlayerSongChange(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const bot_id = parseInt(json["bot_id"]);
|
||||
const client = this.connection.client.channelTree.find_client_by_dbid(bot_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const song_id = parseInt(json["song_id"]);
|
||||
let song: SongInfo;
|
||||
if(song_id) {
|
||||
song = new SongInfo();
|
||||
JSON.map_to(song, json);
|
||||
}
|
||||
|
||||
client.events.fire("music_song_change", {
|
||||
song: song
|
||||
});
|
||||
}
|
||||
|
||||
handleNotifyPlaylistSongAdd(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const playlist_id = parseInt(json["playlist_id"]);
|
||||
const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received playlist song add event, but we've no music bot for the playlist (%d)"), playlist_id);
|
||||
return;
|
||||
}
|
||||
|
||||
client.events.fire("playlist_song_add", {
|
||||
song: {
|
||||
song_id: parseInt(json["song_id"]),
|
||||
song_invoker: json["song_invoker"],
|
||||
song_previous_song_id: parseInt(json["song_previous_song_id"]),
|
||||
song_url: json["song_url"],
|
||||
song_url_loader: json["song_url_loader"],
|
||||
|
||||
song_loaded: json["song_loaded"] == true || json["song_loaded"] == "1",
|
||||
song_metadata: json["song_metadata"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleNotifyPlaylistSongRemove(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const playlist_id = parseInt(json["playlist_id"]);
|
||||
const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received playlist song remove event, but we've no music bot for the playlist (%d)"), playlist_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const song_id = parseInt(json["song_id"]);
|
||||
client.events.fire("playlist_song_remove", { song_id: song_id });
|
||||
}
|
||||
|
||||
handleNotifyPlaylistSongReorder(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const playlist_id = parseInt(json["playlist_id"]);
|
||||
const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received playlist song reorder event, but we've no music bot for the playlist (%d)"), playlist_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const song_id = parseInt(json["song_id"]);
|
||||
const previous_song_id = parseInt(json["song_previous_song_id"]);
|
||||
client.events.fire("playlist_song_reorder", { song_id: song_id, previous_song_id: previous_song_id });
|
||||
}
|
||||
|
||||
handleNotifyPlaylistSongLoaded(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
const playlist_id = parseInt(json["playlist_id"]);
|
||||
const client = this.connection.client.channelTree.clients.find(e => e instanceof MusicClientEntry && e.properties.client_playlist_id === playlist_id);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received playlist song loaded event, but we've no music bot for the playlist (%d)"), playlist_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const song_id = parseInt(json["song_id"]);
|
||||
client.events.fire("playlist_song_loaded", {
|
||||
song_id: song_id,
|
||||
success: json["success"] == 1,
|
||||
error_msg: json["load_error_msg"],
|
||||
metadata: json["song_metadata"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,169 @@
|
|||
namespace events {
|
||||
export interface EventConvert<All> {
|
||||
as<T extends keyof All>() : All[T];
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
readonly type: T;
|
||||
}
|
||||
|
||||
export class SingletonEvent implements Event<"singletone-instance"> {
|
||||
static readonly instance = new SingletonEvent();
|
||||
|
||||
readonly type = "singletone-instance";
|
||||
private constructor() { }
|
||||
}
|
||||
|
||||
export class Registry<Events> {
|
||||
private handler: {[key: string]:((event) => void)[]} = {};
|
||||
private connections: {[key: string]:Registry<string>[]} = {};
|
||||
private debug_prefix = undefined;
|
||||
|
||||
enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; }
|
||||
disable_debug() { this.debug_prefix = undefined; }
|
||||
|
||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<T> & EventConvert<Events>) => void);
|
||||
on(events: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void);
|
||||
on(events, handler) {
|
||||
if(!Array.isArray(events))
|
||||
events = [events];
|
||||
|
||||
for(const event of events) {
|
||||
const handlers = this.handler[event] || (this.handler[event] = []);
|
||||
handlers.push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
off(handler: (event?: Event<T>) => void);
|
||||
off(event: keyof Events, handler: (event?: Event<T> & EventConvert<Events>) => void);
|
||||
off(event: (keyof Events)[], handler: (event?: Event<T> & EventConvert<Events>) => void);
|
||||
off(handler_or_events, handler?) {
|
||||
if(typeof handler_or_events === "function") {
|
||||
for(const key of Object.keys(this.handler))
|
||||
this.handler[key].remove(handler);
|
||||
} else {
|
||||
if(!Array.isArray(handler_or_events))
|
||||
handler_or_events = [handler_or_events];
|
||||
|
||||
for(const event of handler_or_events) {
|
||||
const handlers = this.handler[event];
|
||||
if(handlers) handlers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) {
|
||||
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
||||
}
|
||||
|
||||
disconnect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) {
|
||||
(this.connections[event as string] || []).remove(target as any);
|
||||
}
|
||||
|
||||
disconnect_all<EOther>(target: Registry<EOther>) {
|
||||
for(const event of Object.keys(this.connections))
|
||||
this.connections[event].remove(target as any);
|
||||
}
|
||||
|
||||
fire<T extends keyof Events>(event_type: T, data?: Events[T]) {
|
||||
if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type);
|
||||
|
||||
const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, {
|
||||
type: event_type,
|
||||
as: function () { return this; }
|
||||
});
|
||||
|
||||
for(const handler of (this.handler[event_type as string] || []))
|
||||
handler(event);
|
||||
for(const evhandler of (this.connections[event_type as string] || []))
|
||||
evhandler.fire(event_type as any, event as any);
|
||||
}
|
||||
|
||||
destory() {
|
||||
this.handler = {};
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Use a global event bus as event distribute system */
|
||||
namespace event {
|
||||
namespace global {
|
||||
|
||||
}
|
||||
|
||||
export namespace channel_tree {
|
||||
export interface client {
|
||||
"enter_view": {},
|
||||
"left_view": {},
|
||||
|
||||
"property_update": {
|
||||
properties: string[]
|
||||
},
|
||||
|
||||
"music_status_update": {
|
||||
player_buffered_index: number,
|
||||
player_replay_index: number
|
||||
},
|
||||
"music_song_change": {
|
||||
"song": SongInfo
|
||||
},
|
||||
|
||||
/* TODO: Move this out of the music bots interface? */
|
||||
"playlist_song_add": { song: PlaylistSong },
|
||||
"playlist_song_remove": { song_id: number },
|
||||
"playlist_song_reorder": { song_id: number, previous_song_id: number },
|
||||
"playlist_song_loaded": { song_id: number, success: boolean, error_msg?: string, metadata?: string },
|
||||
}
|
||||
}
|
||||
|
||||
export namespace sidebar {
|
||||
export interface music {
|
||||
"open": {}, /* triggers when frame should be shown */
|
||||
"close": {}, /* triggers when frame will be closed */
|
||||
|
||||
"bot_change": {
|
||||
old: MusicClientEntry | undefined,
|
||||
new: MusicClientEntry | undefined
|
||||
},
|
||||
"bot_property_update": {
|
||||
properties: string[]
|
||||
},
|
||||
|
||||
"action_play": {},
|
||||
"action_pause": {},
|
||||
"action_song_set": { song_id: number },
|
||||
"action_forward": {},
|
||||
"action_rewind": {},
|
||||
"action_forward_ms": {
|
||||
units: number;
|
||||
},
|
||||
"action_rewind_ms": {
|
||||
units: number;
|
||||
},
|
||||
"action_song_add": {},
|
||||
"action_song_delete": { song_id: number },
|
||||
"action_playlist_reload": {},
|
||||
|
||||
"playtime_move_begin": {},
|
||||
"playtime_move_end": {
|
||||
canceled: boolean,
|
||||
target_time?: number
|
||||
},
|
||||
|
||||
"reorder_begin": { song_id: number; entry: JQuery },
|
||||
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
||||
|
||||
"player_time_update": channel_tree.client["music_status_update"],
|
||||
"player_song_change": channel_tree.client["music_song_change"],
|
||||
|
||||
"playlist_song_add": channel_tree.client["playlist_song_add"] & { insert_effect?: boolean },
|
||||
"playlist_song_remove": channel_tree.client["playlist_song_remove"],
|
||||
"playlist_song_reorder": channel_tree.client["playlist_song_reorder"],
|
||||
"playlist_song_loaded": channel_tree.client["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const eclient = new events.Registry<events.channel_tree.client>();
|
||||
const emusic = new events.Registry<events.sidebar.music>();
|
||||
|
||||
eclient.connect("playlist_song_loaded", emusic);
|
||||
eclient.connect("playlist_song_loaded", emusic);
|
||||
|
|
|
@ -61,7 +61,7 @@ if(!JSON.map_to) {
|
|||
|
||||
let updates = 0;
|
||||
for (let field of variables) {
|
||||
if (!json[field]) {
|
||||
if (typeof json[field] === "undefined") {
|
||||
console.trace(tr("Json does not contains %s"), field);
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,8 @@ class ClientConnectionInfo {
|
|||
}
|
||||
|
||||
class ClientEntry {
|
||||
readonly events: events.Registry<events.channel_tree.client>;
|
||||
|
||||
protected _clientId: number;
|
||||
protected _channel: ChannelEntry;
|
||||
protected _tag: JQuery<HTMLElement>;
|
||||
|
@ -133,6 +135,8 @@ class ClientEntry {
|
|||
channelTree: ChannelTree;
|
||||
|
||||
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
|
||||
this.events = new events.Registry<events.channel_tree.client>();
|
||||
|
||||
this._properties = properties;
|
||||
this._properties.client_nickname = clientName;
|
||||
this._clientId = clientId;
|
||||
|
@ -651,7 +655,7 @@ class ClientEntry {
|
|||
visible: this._audio_muted,
|
||||
callback: () => this.set_muted(false, true)
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {})
|
||||
contextmenu.Entry.CLOSE(() => trigger_close && on_close ? on_close() : {})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -929,6 +933,9 @@ class ClientEntry {
|
|||
}
|
||||
|
||||
group.end();
|
||||
this.events.fire("property_update", {
|
||||
properties: variables.map(e => e.key)
|
||||
});
|
||||
}
|
||||
|
||||
update_displayed_client_groups() {
|
||||
|
@ -1232,7 +1239,40 @@ class MusicClientProperties extends ClientProperties {
|
|||
client_disabled: boolean = false;
|
||||
}
|
||||
|
||||
class MusicClientPlayerInfo {
|
||||
/*
|
||||
* command[index]["song_id"] = element ? element->getSongId() : 0;
|
||||
command[index]["song_url"] = element ? element->getUrl() : "";
|
||||
command[index]["song_invoker"] = element ? element->getInvoker() : 0;
|
||||
command[index]["song_loaded"] = false;
|
||||
|
||||
auto entry = dynamic_pointer_cast<ts::music::PlayableSong>(element);
|
||||
if(entry) {
|
||||
auto data = entry->song_loaded_data();
|
||||
command[index]["song_loaded"] = entry->song_loaded() && data;
|
||||
|
||||
if(entry->song_loaded() && data) {
|
||||
command[index]["song_title"] = data->title;
|
||||
command[index]["song_description"] = data->description;
|
||||
command[index]["song_thumbnail"] = data->thumbnail;
|
||||
command[index]["song_length"] = data->length.count();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
class SongInfo {
|
||||
song_id: number = 0;
|
||||
song_url: string = "";
|
||||
song_invoker: number = 0;
|
||||
song_loaded: boolean = false;
|
||||
|
||||
/* only if song_loaded = true */
|
||||
song_title: string = "";
|
||||
song_description: string = "";
|
||||
song_thumbnail: string = "";
|
||||
song_length: number = 0;
|
||||
}
|
||||
|
||||
class MusicClientPlayerInfo extends SongInfo {
|
||||
bot_id: number = 0;
|
||||
player_state: number = 0;
|
||||
|
||||
|
@ -1243,14 +1283,6 @@ class MusicClientPlayerInfo {
|
|||
|
||||
player_title: string = "";
|
||||
player_description: string = "";
|
||||
|
||||
song_id: number = 0;
|
||||
song_url: string = "";
|
||||
song_invoker: number = 0;
|
||||
song_loaded: boolean = false;
|
||||
song_title: string = "";
|
||||
song_thumbnail: string = "";
|
||||
song_length: number = 0;
|
||||
}
|
||||
|
||||
class MusicClientEntry extends ClientEntry {
|
||||
|
@ -1456,7 +1488,7 @@ class MusicClientEntry extends ClientEntry {
|
|||
},
|
||||
type: contextmenu.MenuEntryType.ENTRY
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close && on_close())
|
||||
contextmenu.Entry.CLOSE(() => trigger_close && on_close ? on_close() : {})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1471,19 +1503,12 @@ class MusicClientEntry extends ClientEntry {
|
|||
if(this._info_promise_resolve)
|
||||
this._info_promise_resolve(info);
|
||||
this._info_promise_reject = undefined;
|
||||
}
|
||||
if(this._info_promise) {
|
||||
if(this._info_promise_reject)
|
||||
this._info_promise_reject("timeout");
|
||||
this._info_promise = undefined;
|
||||
this._info_promise_age = undefined;
|
||||
this._info_promise_reject = undefined;
|
||||
this._info_promise_resolve = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
requestPlayerInfo(max_age: number = 1000) : Promise<MusicClientPlayerInfo> {
|
||||
if(this._info_promise && this._info_promise_age && Date.now() - max_age <= this._info_promise_age) return this._info_promise;
|
||||
if(this._info_promise !== undefined && this._info_promise_age > 0 && Date.now() - max_age <= this._info_promise_age) return this._info_promise;
|
||||
this._info_promise_age = Date.now();
|
||||
this._info_promise = new Promise<MusicClientPlayerInfo>((resolve, reject) => {
|
||||
this._info_promise_reject = reject;
|
||||
|
|
|
@ -65,10 +65,9 @@ namespace chat {
|
|||
this.handle.show_private_conversations();
|
||||
})[0];
|
||||
|
||||
this._button_bot_manage = this._html_tag.find(".button-bot-manage");
|
||||
this._button_song_add = this._html_tag.find(".button-bot-manage").find(".bot-song-add").on('click', event => {
|
||||
//TODO!
|
||||
console.log("Add a song");
|
||||
this._button_bot_manage = this._html_tag.find(".bot-manage");
|
||||
this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => {
|
||||
this.handle.music_info().events.fire("action_song_add");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -224,11 +223,9 @@ namespace chat {
|
|||
}
|
||||
|
||||
set_mode(mode: InfoFrameMode) {
|
||||
if(this._mode !== mode) {
|
||||
this._mode = mode;
|
||||
this._html_tag.find(".mode-based").hide();
|
||||
this._html_tag.find(".mode-" + mode).show();
|
||||
}
|
||||
for(const mode in InfoFrameMode)
|
||||
this._html_tag.removeClass("mode-" + InfoFrameMode[mode]);
|
||||
this._html_tag.addClass("mode-" + mode);
|
||||
|
||||
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
|
||||
//Will be called every time a client is shown
|
||||
|
@ -385,9 +382,6 @@ namespace chat {
|
|||
}
|
||||
|
||||
show_music_player(client: MusicClientEntry) {
|
||||
this.show_client_info(client);
|
||||
return;
|
||||
|
||||
this._music_info.set_current_bot(client);
|
||||
|
||||
if(this._content_type === FrameContent.MUSIC_BOT)
|
||||
|
|
|
@ -1,16 +1,55 @@
|
|||
namespace chat {
|
||||
import PlayerState = connection.voice.PlayerState;
|
||||
|
||||
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
|
||||
interface LoadedSongData {
|
||||
description: string;
|
||||
title: string;
|
||||
url: string;
|
||||
|
||||
length: number;
|
||||
thumbnail?: string;
|
||||
|
||||
metadata: {[key: string]: string};
|
||||
}
|
||||
|
||||
export class MusicInfo {
|
||||
readonly events: events.Registry<events.sidebar.music>;
|
||||
readonly handle: Frame;
|
||||
|
||||
private _html_tag: JQuery;
|
||||
private _container_playlist: JQuery;
|
||||
|
||||
private _current_bot: MusicClientEntry | undefined;
|
||||
private update_song_info: number = 0; /* timestamp when we force update the info */
|
||||
private time_select: {
|
||||
active: boolean,
|
||||
max_time: number,
|
||||
current_select_time: number,
|
||||
current_player_time: number
|
||||
} = { active: false, current_select_time: 0, max_time: 0, current_player_time: 0};
|
||||
private song_reorder: {
|
||||
active: boolean,
|
||||
song_id: number,
|
||||
previous_entry: number,
|
||||
html: JQuery,
|
||||
mouse?: {x: number, y: number},
|
||||
indicator: JQuery
|
||||
} = { active: false, song_id: 0, previous_entry: 0, html: undefined, indicator: $.spawn("div").addClass("reorder-indicator") };
|
||||
|
||||
previous_frame_content: FrameContent;
|
||||
|
||||
constructor(handle: Frame) {
|
||||
this.events = new events.Registry<events.sidebar.music>();
|
||||
this.handle = handle;
|
||||
|
||||
this.events.enable_debug("music-info");
|
||||
this.initialize_listener();
|
||||
this._build_html_tag();
|
||||
|
||||
this.set_current_bot(undefined, true);
|
||||
}
|
||||
|
||||
html_tag() : JQuery {
|
||||
|
@ -18,6 +57,9 @@ namespace chat {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this.set_current_bot(undefined);
|
||||
this.events.destory();
|
||||
|
||||
this._html_tag && this._html_tag.remove();
|
||||
this._html_tag = undefined;
|
||||
|
||||
|
@ -25,14 +67,579 @@ namespace chat {
|
|||
this.previous_frame_content = undefined;
|
||||
}
|
||||
|
||||
private format_time(value: number) {
|
||||
if(value == 0) return "--:--:--";
|
||||
|
||||
value /= 1000;
|
||||
|
||||
let hours = 0, minutes = 0;
|
||||
while(value >= 60 * 60) {
|
||||
hours++;
|
||||
value -= 60 * 60;
|
||||
}
|
||||
|
||||
while(value >= 60) {
|
||||
minutes++;
|
||||
value -= 60;
|
||||
}
|
||||
|
||||
return ("0" + hours).substr(-2) + ":" + ("0" + minutes).substr(-2) + ":" + ("0" + value.toFixed(0)).substr(-2);
|
||||
};
|
||||
|
||||
private _build_html_tag() {
|
||||
this._html_tag = $("#tmpl_frame_chat_music_info").renderTag();
|
||||
this._container_playlist = this._html_tag.find(".container-playlist");
|
||||
|
||||
this._html_tag.find(".button-close").on('click', () => {
|
||||
if(this.previous_frame_content === FrameContent.CLIENT_INFO)
|
||||
this.previous_frame_content = FrameContent.NONE;
|
||||
|
||||
this.handle.set_content(this.previous_frame_content);
|
||||
});
|
||||
|
||||
this._html_tag.find(".button-reload-playlist").on('click', () => this.events.fire("action_playlist_reload"));
|
||||
this._html_tag.find(".button-song-add").on('click', () => this.events.fire("action_song_add"));
|
||||
|
||||
{
|
||||
const button_play = this._html_tag.find(".control-buttons .button-play");
|
||||
const button_pause = this._html_tag.find(".control-buttons .button-pause");
|
||||
|
||||
button_play.on('click', () => this.events.fire("action_play"));
|
||||
button_pause.on('click', () => this.events.fire("action_pause"));
|
||||
|
||||
this.events.on(["bot_change", "bot_property_update"], event => {
|
||||
if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return;
|
||||
|
||||
button_play.toggleClass("hidden", this._current_bot === undefined || this._current_bot.properties.player_state < PlayerState.STOPPING);
|
||||
});
|
||||
|
||||
this.events.on(["bot_change", "bot_property_update"], event => {
|
||||
if(event.type === "bot_property_update" && event.as<"bot_property_update">().properties.indexOf("player_state") == -1) return;
|
||||
|
||||
button_pause.toggleClass("hidden", this._current_bot !== undefined && this._current_bot.properties.player_state >= PlayerState.STOPPING);
|
||||
});
|
||||
|
||||
this._html_tag.find(".control-buttons .button-rewind").on('click', () => this.events.fire("action_rewind"));
|
||||
this._html_tag.find(".control-buttons .button-forward").on('click', () => this.events.fire("action_forward"));
|
||||
}
|
||||
|
||||
/* timeline updaters */
|
||||
{
|
||||
const container = this._html_tag.find(".container-timeline");
|
||||
|
||||
const timeline = container.find(".timeline");
|
||||
const indicator_playtime = container.find(".indicator-playtime");
|
||||
const indicator_buffered = container.find(".indicator-buffered");
|
||||
const thumb = container.find(".thumb");
|
||||
|
||||
const timestamp_current = container.find(".timestamps .current");
|
||||
const timestamp_max = container.find(".timestamps .max");
|
||||
|
||||
thumb.on('mousedown', event => event.button === 0 && this.events.fire("playtime_move_begin"));
|
||||
|
||||
this.events.on(["bot_change", "player_song_change", "player_time_update", "playtime_move_end"], event => {
|
||||
if(!this._current_bot) {
|
||||
this.time_select.max_time = 0;
|
||||
indicator_buffered.each((_, e) => { e.style.width = "0%"; });
|
||||
indicator_playtime.each((_, e) => { e.style.width = "0%"; });
|
||||
thumb.each((_, e) => { e.style.marginLeft = "0%"; });
|
||||
|
||||
timestamp_current.text("--:--:--");
|
||||
timestamp_max.text("--:--:--");
|
||||
return;
|
||||
}
|
||||
if(event.type === "playtime_move_end" && !event.as<"playtime_move_end">().canceled) return;
|
||||
|
||||
const update_info = Date.now() > this.update_song_info;
|
||||
this._current_bot.requestPlayerInfo(update_info ? 1000 : 60 * 1000).then(data => {
|
||||
if(update_info)
|
||||
this.display_song_info(data);
|
||||
|
||||
let played, buffered;
|
||||
if(event.type !== "player_time_update") {
|
||||
played = data.player_replay_index;
|
||||
buffered = data.player_buffered_index;
|
||||
} else {
|
||||
played = event.as<"player_time_update">().player_replay_index;
|
||||
buffered = event.as<"player_time_update">().player_buffered_index;
|
||||
}
|
||||
|
||||
this.time_select.current_player_time = played;
|
||||
this.time_select.max_time = data.player_max_index;
|
||||
timestamp_max.text(data.player_max_index ? this.format_time(data.player_max_index) : "--:--:--");
|
||||
|
||||
if(this.time_select.active)
|
||||
return;
|
||||
|
||||
let wplayed, wbuffered;
|
||||
if(data.player_max_index) {
|
||||
wplayed = (played * 100 / data.player_max_index).toFixed(2) + "%";
|
||||
wbuffered = (buffered * 100 / data.player_max_index).toFixed(2) + "%";
|
||||
|
||||
timestamp_current.text(this.format_time(played));
|
||||
} else {
|
||||
wplayed = "100%";
|
||||
wbuffered = "100%";
|
||||
|
||||
timestamp_current.text(this.format_time(played));
|
||||
}
|
||||
|
||||
indicator_buffered.each((_, e) => { e.style.width = wbuffered; });
|
||||
indicator_playtime.each((_, e) => { e.style.width = wplayed; });
|
||||
thumb.each((_, e) => { e.style.marginLeft = wplayed; });
|
||||
});
|
||||
});
|
||||
|
||||
const move_callback = (event: MouseEvent) => {
|
||||
const x_min = timeline.offset().left;
|
||||
const x_max = x_min + timeline.width();
|
||||
|
||||
let current = event.pageX;
|
||||
if(current < x_min)
|
||||
current = x_min;
|
||||
else if(current > x_max)
|
||||
current = x_max;
|
||||
|
||||
const percent = (current - x_min) / (x_max - x_min);
|
||||
this.time_select.current_select_time = percent * this.time_select.max_time;
|
||||
timestamp_current.text(this.format_time(this.time_select.current_select_time));
|
||||
|
||||
const w = (percent * 100).toFixed(2) + "%";
|
||||
indicator_playtime.each((_, e) => { e.style.width = w; });
|
||||
thumb.each((_, e) => { e.style.marginLeft = w; });
|
||||
};
|
||||
|
||||
const up_callback = (event: MouseEvent | FocusEvent) => {
|
||||
if(event.type === "mouseup")
|
||||
if((event as MouseEvent).button !== 0) return;
|
||||
|
||||
this.events.fire("playtime_move_end", {
|
||||
canceled: event.type !== "mouseup",
|
||||
target_time: this.time_select.current_select_time
|
||||
});
|
||||
};
|
||||
|
||||
this.events.on("playtime_move_begin", event => {
|
||||
if(this.time_select.max_time <= 0) return;
|
||||
|
||||
this.time_select.active = true;
|
||||
indicator_buffered.each((_, e) => { e.style.width = "0"; });
|
||||
document.addEventListener("mousemove", move_callback);
|
||||
document.addEventListener("mouseleave", up_callback);
|
||||
document.addEventListener("blur", up_callback);
|
||||
document.addEventListener("mouseup", up_callback);
|
||||
document.body.style.userSelect = "none";
|
||||
});
|
||||
|
||||
this.events.on(["bot_change", "player_song_change", "playtime_move_end"], event => {
|
||||
document.removeEventListener("mousemove", move_callback);
|
||||
document.removeEventListener("mouseleave", up_callback);
|
||||
document.removeEventListener("blur", up_callback);
|
||||
document.removeEventListener("mouseup", up_callback);
|
||||
document.body.style.userSelect = undefined;
|
||||
this.time_select.active = false;
|
||||
|
||||
if(event.type === "playtime_move_end") {
|
||||
const data = event.as<"playtime_move_end">();
|
||||
if(data.canceled) return;
|
||||
|
||||
const offset = data.target_time - this.time_select.current_player_time;
|
||||
this.events.fire(offset > 0 ? "action_forward_ms" : "action_rewind_ms", {units: Math.abs(offset) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* song info handlers */
|
||||
this.events.on(["bot_change", "player_song_change"], event => {
|
||||
let song: SongInfo;
|
||||
|
||||
/* update the player info so we dont get old data */
|
||||
if(this._current_bot) {
|
||||
this.update_song_info = 0;
|
||||
this._current_bot.requestPlayerInfo(1000).then(data => {
|
||||
this.display_song_info(data);
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to update current song for side bar: %o"), error);
|
||||
});
|
||||
}
|
||||
|
||||
if(event.type === "bot_change") {
|
||||
song = undefined;
|
||||
} else {
|
||||
song = event.as<"player_song_change">().song;
|
||||
}
|
||||
this.display_song_info(song);
|
||||
});
|
||||
}
|
||||
|
||||
private display_song_info(song: SongInfo) {
|
||||
if(song) {
|
||||
if(!song.song_loaded) {
|
||||
console.log("Awaiting a loaded song info.");
|
||||
this.update_song_info = 0;
|
||||
} else {
|
||||
console.log("Song info loaded.");
|
||||
this.update_song_info = Date.now() + 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
if(!song) song = new SongInfo();
|
||||
|
||||
const container_thumbnail = this._html_tag.find(".player .container-thumbnail");
|
||||
const container_info = this._html_tag.find(".player .container-song-info");
|
||||
|
||||
container_thumbnail.find("img").attr("src", song.song_thumbnail || "img/music/no-thumbnail.png");
|
||||
if(song.song_id)
|
||||
container_info.find(".song-name").text(song.song_title || song.song_url).attr("title", song.song_title || song.song_url);
|
||||
else
|
||||
container_info.find(".song-name").text(tr("No song selected"));
|
||||
if(song.song_description) {
|
||||
container_info.find(".song-description").removeClass("hidden").text(song.song_description).attr("title", song.song_description);
|
||||
} else {
|
||||
container_info.find(".song-description").addClass("hidden").text(tr("Song has no description")).attr("title", tr("Song has no description"));
|
||||
}
|
||||
}
|
||||
|
||||
private initialize_listener() {
|
||||
//Must come at first!
|
||||
this.events.on("player_song_change", event => {
|
||||
if(!this._current_bot) return;
|
||||
|
||||
this._current_bot.requestPlayerInfo(0); /* enforce an info refresh */
|
||||
});
|
||||
|
||||
/* bot property listener */
|
||||
const callback_property = event => this.events.fire("bot_property_update", { properties: event.properties });
|
||||
const callback_time_update = event => this.events.fire("player_time_update", event);
|
||||
const callback_song_change = event => this.events.fire("player_song_change", event);
|
||||
this.events.on("bot_change", event => {
|
||||
if(event.old) {
|
||||
event.old.events.off(callback_property);
|
||||
event.old.events.off(callback_time_update);
|
||||
event.old.events.off(callback_song_change);
|
||||
event.old.events.disconnect_all(this.events);
|
||||
}
|
||||
if(event.new) {
|
||||
event.new.events.on("property_update", callback_property);
|
||||
|
||||
event.new.events.on("music_status_update", callback_time_update);
|
||||
event.new.events.on("music_song_change", callback_song_change);
|
||||
|
||||
event.new.events.connect("playlist_song_add", this.events);
|
||||
event.new.events.connect("playlist_song_remove", this.events);
|
||||
event.new.events.connect("playlist_song_reorder", this.events);
|
||||
event.new.events.connect("playlist_song_loaded", this.events);
|
||||
}
|
||||
});
|
||||
|
||||
/* basic player actions */
|
||||
{
|
||||
const action_map = {
|
||||
"action_play": 1,
|
||||
"action_pause": 2,
|
||||
"action_forward": 3,
|
||||
"action_rewind": 4,
|
||||
"action_forward_ms": 5,
|
||||
"action_rewind_ms": 6
|
||||
};
|
||||
|
||||
this.events.on(Object.keys(action_map) as any, event => {
|
||||
if(!this._current_bot) return;
|
||||
|
||||
const action_id = action_map[event.type];
|
||||
if(typeof action_id === "undefined") {
|
||||
log.warn(LogCategory.GENERAL, tr("Invalid music bot action event detected: %s. This should not happen!"), event.type);
|
||||
return;
|
||||
}
|
||||
const data = Object.assign({
|
||||
bot_id: this._current_bot.properties.client_database_id,
|
||||
action: action_id
|
||||
}, event);
|
||||
this.handle.handle.serverConnection.send_command("musicbotplayeraction", data).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return;
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Failed to perform action %s on bot: %o"), event.type, error);
|
||||
//TODO: Better error dialog
|
||||
createErrorModal(tr("Failed to perform action."), tr("Failed to perform action for music bot.")).open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.events.on("action_song_set", event => {
|
||||
if(!this._current_bot) return;
|
||||
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
if(!connection || !connection.connected()) return;
|
||||
|
||||
connection.send_command("playlistsongsetcurrent", {
|
||||
playlist_id: this._current_bot.properties.client_playlist_id,
|
||||
song_id: event.song_id
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return;
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Failed to set current song on bot: %o"), event.type, error);
|
||||
//TODO: Better error dialog
|
||||
createErrorModal(tr("Failed to set song."), tr("Failed to set current replaying song.")).open();
|
||||
})
|
||||
});
|
||||
|
||||
this.events.on("action_song_add", () => {
|
||||
if(!this._current_bot) return;
|
||||
|
||||
createInputModal(tr("Enter song URL"), tr("Please enter the target song URL"), text => {
|
||||
try {
|
||||
new URL(text);
|
||||
return true;
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
}, result => {
|
||||
if(!result || !this._current_bot) return;
|
||||
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
connection.send_command("playlistsongadd", {
|
||||
playlist_id: this._current_bot.properties.client_playlist_id,
|
||||
url: result
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return;
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error);
|
||||
|
||||
//TODO: Better error description
|
||||
createErrorModal(tr("Failed to insert song"), tr("Failed to append song to the playlist.")).open();
|
||||
});
|
||||
}).open();
|
||||
});
|
||||
|
||||
this.events.on("action_song_delete", event => {
|
||||
if(!this._current_bot) return;
|
||||
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
if(!connection || !connection.connected()) return;
|
||||
|
||||
connection.send_command("playlistsongremove", {
|
||||
playlist_id: this._current_bot.properties.client_playlist_id,
|
||||
song_id: event.song_id
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return;
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Failed to delete song from bot playlist: %o"), error);
|
||||
|
||||
//TODO: Better error description
|
||||
createErrorModal(tr("Failed to delete song"), tr("Failed to remove song from the playlist.")).open();
|
||||
});
|
||||
});
|
||||
|
||||
/* bot subscription */
|
||||
this.events.on("bot_change", () => {
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
if(!connection || !connection.connected()) return;
|
||||
|
||||
const bot_id = this._current_bot ? this._current_bot.properties.client_database_id : 0;
|
||||
this.handle.handle.serverConnection.send_command("musicbotsetsubscription", { bot_id: bot_id }).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to subscribe to displayed bot within the side bar: %o"), error);
|
||||
});
|
||||
});
|
||||
|
||||
/* playlist stuff */
|
||||
this.events.on(["bot_change", "action_playlist_reload"], event => {
|
||||
this.playlist_subscribe(true);
|
||||
this.update_playlist();
|
||||
});
|
||||
|
||||
this.events.on("playlist_song_add", event => {
|
||||
const animation = typeof event.insert_effect === "boolean" ? event.insert_effect : true;
|
||||
const html_entry = this.build_playlist_entry(event.song, animation);
|
||||
const playlist = this._container_playlist.find(".playlist");
|
||||
const previous = playlist.find(".entry[song-id=" + event.song.song_previous_song_id + "]");
|
||||
|
||||
if(previous.length)
|
||||
html_entry.insertAfter(previous);
|
||||
else
|
||||
html_entry.appendTo(playlist);
|
||||
if(event.song.song_loaded)
|
||||
this.events.fire("playlist_song_loaded", {
|
||||
html_entry: html_entry,
|
||||
metadata: event.song.song_metadata,
|
||||
success: true,
|
||||
song_id: event.song.song_id
|
||||
});
|
||||
if(animation)
|
||||
setTimeout(() => html_entry.addClass("shown"), 50);
|
||||
});
|
||||
|
||||
this.events.on("playlist_song_remove", event => {
|
||||
const playlist = this._container_playlist.find(".playlist");
|
||||
const song = playlist.find(".entry[song-id=" + event.song_id + "]");
|
||||
song.addClass("deleted");
|
||||
setTimeout(() => song.remove(), 5000); /* to play some animations */
|
||||
});
|
||||
|
||||
this.events.on("playlist_song_reorder", event => {
|
||||
const playlist = this._container_playlist.find(".playlist");
|
||||
const entry = playlist.find(".entry[song-id=" + event.song_id + "]");
|
||||
if(!entry) return;
|
||||
|
||||
console.log(event);
|
||||
const previous = playlist.find(".entry[song-id=" + event.previous_song_id + "]");
|
||||
if(previous.length) {
|
||||
entry.insertAfter(previous);
|
||||
} else {
|
||||
entry.insertBefore(playlist.find(".entry")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on("playlist_song_loaded", event => {
|
||||
const entry = event.html_entry || this._container_playlist.find(".playlist .entry[song-id=" + event.song_id + "]");
|
||||
|
||||
const thumbnail = entry.find(".container-thumbnail img");
|
||||
const name = entry.find(".name");
|
||||
const description = entry.find(".description");
|
||||
const length = entry.find(".length");
|
||||
|
||||
if(event.success) {
|
||||
let meta: LoadedSongData;
|
||||
try {
|
||||
meta = JSON.parse(event.metadata);
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to decode song metadata"));
|
||||
meta = {
|
||||
description: "",
|
||||
title: "",
|
||||
metadata: {},
|
||||
length: 0,
|
||||
url: entry.attr("song-url")
|
||||
}
|
||||
}
|
||||
|
||||
if(!meta.title && meta.description) {
|
||||
meta.title = meta.description.split("\n")[0];
|
||||
meta.description = meta.description.split("\n").slice(1).join("\n");
|
||||
}
|
||||
meta.title = meta.title || meta.url;
|
||||
|
||||
name.text(meta.title);
|
||||
description.text(meta.description);
|
||||
length.text(this.format_time(meta.length || 0));
|
||||
if(meta.thumbnail)
|
||||
thumbnail.attr("src", meta.thumbnail);
|
||||
} else {
|
||||
name.text(tr("failed to load ") + entry.attr("song-url")).attr("title", tr("failed to load ") + entry.attr("song-url"));
|
||||
description.text(event.error_msg || tr("unknown error")).attr("title", event.error_msg || tr("unknown error"));
|
||||
}
|
||||
});
|
||||
|
||||
/* song reorder */
|
||||
{
|
||||
const move_callback = (event: MouseEvent) => {
|
||||
if(!this.song_reorder.html) return;
|
||||
|
||||
this.song_reorder.html.each((_, e) => {
|
||||
e.style.left = (event.pageX - this.song_reorder.mouse.x) + "px";
|
||||
e.style.top = (event.pageY - this.song_reorder.mouse.y) + "px";
|
||||
});
|
||||
|
||||
const entries = this._container_playlist.find(".playlist .entry");
|
||||
let before: HTMLElement;
|
||||
for(const entry of entries) {
|
||||
const off = $(entry).offset().top;
|
||||
if(off > event.pageY) {
|
||||
this.song_reorder.indicator.insertBefore(entry);
|
||||
this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0;
|
||||
return;
|
||||
}
|
||||
|
||||
before = entry;
|
||||
}
|
||||
this.song_reorder.indicator.insertAfter(entries.last());
|
||||
this.song_reorder.previous_entry = before ? parseInt(before.attributes.getNamedItem("song-id").value) : 0;
|
||||
};
|
||||
|
||||
const up_callback = (event: MouseEvent | FocusEvent) => {
|
||||
if(event.type === "mouseup")
|
||||
if((event as MouseEvent).button !== 0) return;
|
||||
|
||||
this.events.fire("reorder_end", {
|
||||
canceled: event.type !== "mouseup",
|
||||
song_id: this.song_reorder.song_id,
|
||||
entry: this.song_reorder.html,
|
||||
previous_entry: this.song_reorder.previous_entry
|
||||
});
|
||||
};
|
||||
|
||||
this.events.on("reorder_begin", event => {
|
||||
this.song_reorder.song_id = event.song_id;
|
||||
this.song_reorder.html = event.entry;
|
||||
|
||||
const width = this.song_reorder.html.width() + "px";
|
||||
this.song_reorder.html.each((_, e) => { e.style.width = width; });
|
||||
this.song_reorder.active = true;
|
||||
this.song_reorder.html.addClass("reordering");
|
||||
|
||||
document.addEventListener("mousemove", move_callback);
|
||||
document.addEventListener("mouseleave", up_callback);
|
||||
document.addEventListener("blur", up_callback);
|
||||
document.addEventListener("mouseup", up_callback);
|
||||
document.body.style.userSelect = "none";
|
||||
});
|
||||
|
||||
this.events.on(["bot_change", "playlist_song_remove", "reorder_end"], event => {
|
||||
if(event.type === "playlist_song_remove" && event.as<"playlist_song_remove">().song_id !== this.song_reorder.song_id) return;
|
||||
|
||||
document.removeEventListener("mousemove", move_callback);
|
||||
document.removeEventListener("mouseleave", up_callback);
|
||||
document.removeEventListener("blur", up_callback);
|
||||
document.removeEventListener("mouseup", up_callback);
|
||||
document.body.style.userSelect = undefined;
|
||||
|
||||
this.song_reorder.active = false;
|
||||
this.song_reorder.indicator.remove();
|
||||
if(this.song_reorder.html) {
|
||||
this.song_reorder.html.each((_, e) => {
|
||||
e.style.width = null;
|
||||
e.style.left = null;
|
||||
e.style.top = null;
|
||||
});
|
||||
this.song_reorder.html.removeClass("reordering");
|
||||
}
|
||||
|
||||
if(event.type === "reorder_end") {
|
||||
const data = event.as<"reorder_end">();
|
||||
if(data.canceled) return;
|
||||
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
if(!connection || !connection.connected()) return;
|
||||
if(!this._current_bot) return;
|
||||
|
||||
connection.send_command("playlistsongreorder", {
|
||||
playlist_id: this._current_bot.properties.client_playlist_id,
|
||||
song_id: data.song_id,
|
||||
song_previous_song_id: data.previous_entry
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return;
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Failed to add song to bot playlist: %o"), error);
|
||||
|
||||
//TODO: Better error description
|
||||
createErrorModal(tr("Failed to reorder song"), tr("Failed to reorder song within the playlist.")).open();
|
||||
});
|
||||
console.log("Reorder to %d", data.previous_entry);
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on(["bot_change", "player_song_change"], event => {
|
||||
if(!this._current_bot) {
|
||||
this._html_tag.find(".playlist .current-song").removeClass("current-song");
|
||||
return;
|
||||
}
|
||||
|
||||
this._current_bot.requestPlayerInfo(1000).then(data => {
|
||||
const song_id = data ? data.song_id : 0;
|
||||
this._html_tag.find(".playlist .current-song").removeClass("current-song");
|
||||
this._html_tag.find(".playlist .entry[song-id=" + song_id + "]").addClass("current-song");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
set_current_bot(client: MusicClientEntry | undefined, enforce?: boolean) {
|
||||
|
@ -40,8 +647,178 @@ namespace chat {
|
|||
if(client === this._current_bot && (typeof(enforce) === "undefined" || !enforce))
|
||||
return;
|
||||
|
||||
const old = this._current_bot;
|
||||
this._current_bot = client;
|
||||
this.events.fire("bot_change", {
|
||||
new: client,
|
||||
old: old
|
||||
});
|
||||
}
|
||||
|
||||
private sort_songs(data: PlaylistSong[]) {
|
||||
const result = [];
|
||||
|
||||
let appendable: PlaylistSong[] = [];
|
||||
for(const song of data) {
|
||||
if(song.song_id == 0 || data.findIndex(e => e.song_id === song.song_previous_song_id) == -1)
|
||||
result.push(song);
|
||||
else
|
||||
appendable.push(song);
|
||||
}
|
||||
|
||||
let iters;
|
||||
while (appendable.length) {
|
||||
do {
|
||||
iters = 0;
|
||||
const left: PlaylistSong[] = [];
|
||||
for(const song of appendable) {
|
||||
const index = data.findIndex(e => e.song_id === song.song_previous_song_id);
|
||||
if(index == -1) {
|
||||
left.push(song);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.splice(index + 1, 0, song);
|
||||
iters++;
|
||||
}
|
||||
appendable = left;
|
||||
} while(iters > 0);
|
||||
|
||||
if(appendable.length)
|
||||
result.push(appendable.pop_front());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* playlist stuff */
|
||||
update_playlist() {
|
||||
this.playlist_subscribe(true);
|
||||
|
||||
this._container_playlist.find(".overlay").toggleClass("hidden", true);
|
||||
const playlist = this._container_playlist.find(".playlist");
|
||||
playlist.empty();
|
||||
|
||||
if(!this.handle.handle.serverConnection || !this.handle.handle.serverConnection.connected() || !this._current_bot) {
|
||||
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay_loading = this._container_playlist.find(".overlay-loading");
|
||||
overlay_loading.removeClass("hidden");
|
||||
|
||||
this._current_bot.updateClientVariables(true).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
||||
}).then(() => {
|
||||
this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id).then(songs => {
|
||||
this.playlist_subscribe(false); /* we're allowed to see the playlist */
|
||||
if(!songs) {
|
||||
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
for(const song of this.sort_songs(songs))
|
||||
this.events.fire("playlist_song_add", { song: song, insert_effect: false });
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) {
|
||||
this._container_playlist.find(".overlay-no-permissions").removeClass("hidden");
|
||||
return;
|
||||
}
|
||||
log.error(LogCategory.CLIENT, tr("Failed to load bot playlist: %o"), error);
|
||||
this._container_playlist.find(".overlay.overlay-error").removeClass("hidden");
|
||||
}).then(() => {
|
||||
overlay_loading.addClass("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _playlist_subscribed = false;
|
||||
private playlist_subscribe(unsubscribe: boolean) {
|
||||
if(!this.handle.handle.serverConnection) return;
|
||||
|
||||
if(unsubscribe || !this._current_bot) {
|
||||
if(!this._playlist_subscribed) return;
|
||||
this._playlist_subscribed = false;
|
||||
|
||||
this.handle.handle.serverConnection.send_command("playlistsetsubscription", {playlist_id: 0}).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to unsubscribe from last playlist: %o"), error);
|
||||
});
|
||||
} else {
|
||||
this.handle.handle.serverConnection.send_command("playlistsetsubscription", {
|
||||
playlist_id: this._current_bot.properties.client_playlist_id
|
||||
}).then(() => this._playlist_subscribed = true).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to subscribe to bots playlist: %o"), error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private build_playlist_entry(data: PlaylistSong, insert_effect: boolean) : JQuery {
|
||||
const tag = $("#tmpl_frame_music_playlist_entry").renderTag();
|
||||
tag.attr({
|
||||
"song-id": data.song_id,
|
||||
"song-url": data.song_url
|
||||
});
|
||||
|
||||
const thumbnail = tag.find(".container-thumbnail img");
|
||||
const name = tag.find(".name");
|
||||
const description = tag.find(".description");
|
||||
const length = tag.find(".length");
|
||||
|
||||
tag.find(".button-delete").on('click', () => this.events.fire("action_song_delete", { song_id: data.song_id }));
|
||||
tag.on('dblclick', event => this.events.fire("action_song_set", { song_id: data.song_id }));
|
||||
name.text(tr("loading..."));
|
||||
description.text(data.song_url);
|
||||
|
||||
tag.on('mousedown', event => {
|
||||
if(event.button !== 0) return;
|
||||
|
||||
this.song_reorder.mouse = {
|
||||
x: event.pageX,
|
||||
y: event.pageY
|
||||
};
|
||||
|
||||
const baseOff = tag.offset();
|
||||
const off = { x: event.pageX - baseOff.left, y: event.pageY - baseOff.top };
|
||||
const move_listener = (event: MouseEvent) => {
|
||||
const distance = Math.pow(event.pageX - this.song_reorder.mouse.x, 2) + Math.pow(event.pageY - this.song_reorder.mouse.y, 2);
|
||||
if(distance < 50) return;
|
||||
|
||||
document.removeEventListener("blur", up_listener);
|
||||
document.removeEventListener("mouseup", up_listener);
|
||||
document.removeEventListener("mousemove", move_listener);
|
||||
|
||||
this.song_reorder.mouse = off;
|
||||
this.events.fire("reorder_begin", {
|
||||
entry: tag,
|
||||
song_id: data.song_id
|
||||
});
|
||||
};
|
||||
|
||||
const up_listener = event => {
|
||||
if(event.type === "mouseup" && event.button !== 0) return;
|
||||
|
||||
document.removeEventListener("blur", up_listener);
|
||||
document.removeEventListener("mouseup", up_listener);
|
||||
document.removeEventListener("mousemove", move_listener);
|
||||
};
|
||||
|
||||
document.addEventListener("blur", up_listener);
|
||||
document.addEventListener("mouseup", up_listener);
|
||||
document.addEventListener("mousemove", move_listener);
|
||||
});
|
||||
|
||||
if(this._current_bot) {
|
||||
this._current_bot.requestPlayerInfo(60 * 1000).then(pdata => {
|
||||
if(pdata.song_id === data.song_id)
|
||||
tag.addClass("current-song");
|
||||
});
|
||||
}
|
||||
|
||||
if(insert_effect) {
|
||||
tag.removeClass("shown");
|
||||
tag.addClass("animation");
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -515,7 +515,7 @@ class ChannelTree {
|
|||
if(!$.isArray(this.currently_selected)) {
|
||||
if(this.currently_selected instanceof ClientEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
|
||||
if(this.currently_selected instanceof MusicClientEntry)
|
||||
this.client.side_bar.show_music_player(this.currently_selected);
|
||||
this.client.side_bar.show_music_player(this.currently_selected as MusicClientEntry);
|
||||
else
|
||||
this.client.side_bar.show_client_info(this.currently_selected);
|
||||
} else if(this.currently_selected instanceof ChannelEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
|
|
|
@ -143,6 +143,7 @@ const loader_javascript = {
|
|||
"js/i18n/localize.js",
|
||||
"js/i18n/country.js",
|
||||
"js/log.js",
|
||||
"js/events.js",
|
||||
|
||||
"js/sound/Sounds.js",
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit aa10cff586a71b5586b2a35564bd2bbe39e19578
|
||||
Subproject commit 4fe112d7ab4e2a9a94afc8cbeecc6e890723e61d
|
Loading…
Reference in New Issue