Reimplemented the music bot manage UI
parent
6efc4ad075
commit
12a7b6eb50
|
@ -1,4 +1,9 @@
|
|||
# Changelog:
|
||||
* **29.12.20**
|
||||
- Reimplemented the music bot control UI
|
||||
- Fixing some bugs from earlier versions
|
||||
- Added a volume change button to easily change the bots volume
|
||||
|
||||
* **22.12.20**
|
||||
- Fixed missing channel status icon update on channel type edit
|
||||
- Improved channel edit UI experience and fixed some bugs
|
||||
|
@ -33,7 +38,7 @@
|
|||
- Enabled context menus for all clickable client tags
|
||||
- Allowing to drag client tags
|
||||
- Fixed the context menu within popout windows for the web client
|
||||
- Reworked the whole sidebar (Hightly decreased memory footprint)
|
||||
- Reworked the whole sidebar (Highly decreased memory footprint)
|
||||
|
||||
* **08.12.20**
|
||||
- Fixed the permission editor not resolving unique ids
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
@import "mixin";
|
||||
@import "properties";
|
||||
|
||||
//$color_client_normal: #bebebe;
|
||||
$color_client_normal: #cccccc;
|
||||
$client_info_avatar_size: 10em;
|
||||
$bot_thumbnail_width: 16em;
|
||||
$bot_thumbnail_height: 9em;
|
||||
|
||||
/* FIXME: Resolve variable usage! */
|
||||
html:root {
|
||||
--side-info-background: #2e2e2e;
|
||||
--side-info-shadow: rgba(0, 0, 0, 0.25);
|
||||
|
@ -26,520 +21,4 @@ html:root {
|
|||
--side-info-ping-very-poor: #7f2222;
|
||||
|
||||
--side-info-bot-add-song: #3f7538;
|
||||
}
|
||||
|
||||
.container-chat-frame {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 200px;
|
||||
|
||||
.container-chat {
|
||||
width: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
|
||||
min-width: 350px;
|
||||
min-height: 16em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.container-music-info {
|
||||
position: relative;
|
||||
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
|
||||
.player {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-thumbnail {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: inline-block;
|
||||
margin: calc(#{$bot_thumbnail_height} / -2) .75em .5em .5em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
border-radius: .5em;
|
||||
overflow: hidden;
|
||||
|
||||
.thumbnail {
|
||||
overflow: hidden;
|
||||
|
||||
width: $bot_thumbnail_width;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-close {
|
||||
font-size: 4em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
opacity: 0.3;
|
||||
|
||||
width: .5em;
|
||||
height: .5em;
|
||||
|
||||
margin-right: .1em;
|
||||
margin-top: .1em;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||
|
||||
&:before, &:after {
|
||||
position: absolute;
|
||||
left: .25em;
|
||||
content: ' ';
|
||||
height: .5em;
|
||||
width: .05em;
|
||||
background-color: #5a5a5a;
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,168 +67,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_chat_info" type="text/html">
|
||||
<div class="lane">
|
||||
<div class="block left">
|
||||
<div class="title">{{tr "You're talking in Channel" /}}</div>
|
||||
<div class="value value-voice-channel"></div>
|
||||
<div class="small-value value-voice-limit"></div>
|
||||
</div>
|
||||
<div class="block right">
|
||||
<div class="title">{{tr "Your ping" /}}</div>
|
||||
<div class="value value-ping">11 ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lane">
|
||||
<div class="block left mode-based mode-channel_chat channel">
|
||||
<div class="title"></div>
|
||||
<div class="value value-text-channel"></div>
|
||||
<div class="small-value value-text-limit"></div>
|
||||
</div>
|
||||
<div class="block left mode-based mode-private_chat">
|
||||
<div class="button button-switch-chat-channel">{{tr "Switch to channel chat" /}}</div>
|
||||
</div>
|
||||
|
||||
<div class="block left mode-based mode-music_bot">
|
||||
<div class="title"> </div>
|
||||
<div class="value button bot-manage">{{tr "Manage Bot" /}}</div>
|
||||
</div>
|
||||
|
||||
<div class="block left mode-based mode-client_info">
|
||||
<div class="spacer-client-info"></div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-based mode-private_chat">
|
||||
<div class="title">{{tr "Private chats" /}}
|
||||
<div class="container-indicator">
|
||||
<div class="chat-unread-counter">-1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value button chat-counter">Hmm... Something seems to be wrong!</div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-basedt mode-channel_chat">
|
||||
<div class="title">{{tr "Private chats" /}}
|
||||
<div class="container-indicator">
|
||||
<div class="chat-unread-counter">-1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value button chat-counter">Hmm... Something seems to be wrong!</div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-based mode-client_info">
|
||||
<div class="title"> </div>
|
||||
<div class="value button open-conversation">error: open conversation</div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-based mode-music_bot">
|
||||
<div class="title"> </div>
|
||||
<div class="value button bot-add-song">{{tr "Add song" /}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_chat_client_info" type="text/html">
|
||||
<div class="container-client-info">
|
||||
<div class="heading">
|
||||
<div class="container-avatar">
|
||||
<div class="avatar">
|
||||
<img src="img/style/avatar.png" style="height: 100%; width: 100%">
|
||||
</div>
|
||||
<div class="container-avatar-edit" title="{{tr 'Upload Avatar' /}}">
|
||||
<img src="img/photo-camera.svg">
|
||||
</div>
|
||||
</div>
|
||||
<a class="client-name"></a>
|
||||
<div class="container-description">
|
||||
<a class="client-description">error: description</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="general-info">
|
||||
<div class="block block-left">
|
||||
<div class="container-property">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_online_time.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Online since" /}}</div>
|
||||
<div class="value client-online-time">error: online-time</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_country.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Country" /}}</div>
|
||||
<div class="value client-country"><a>error: country</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property container-teaforo">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_forum_account.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "TeaSpeak Forum Account" /}}</div>
|
||||
<div class="value client-teaforo-account"><a>error: online-time</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_version.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Version" /}}</div>
|
||||
<div class="value client-version"><a>error: version</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<!-- Using inline style because the icon itself is a bit off. A better way would be to edit the icon itself, but I'm unable to do so.... -->
|
||||
<div class="container-icon" style="margin-right: .16em; margin-left: .04em;">
|
||||
<img src="img/icon_client_volume.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Volume" /}}</div>
|
||||
<div class="value client-local-volume">error: local volume</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property list container-client-status">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_status.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Status" /}}</div>
|
||||
<div class="value client-status">
|
||||
<div>error: client status</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block block-right">
|
||||
<div class="container-property">
|
||||
<div class="icon_em client-permission_server_groups"></div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Channel Group" /}}</div>
|
||||
<div class="value client-group-channel"><a>error: channel group</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property list">
|
||||
<div class="icon_em client-permission_channel"></div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Server Groups" /}}</div>
|
||||
<div class="value client-group-server">
|
||||
<div>error: client server groups</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-close"></div>
|
||||
<div class="button-more">{{tr "Full Info" /}}</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_chat_music_info" type="text/html">
|
||||
<div class="container-music-info">
|
||||
<div class="player">
|
||||
|
@ -370,25 +208,6 @@
|
|||
</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>
|
||||
|
||||
<div class="template-group-modals">
|
||||
<script class="jsrender-template" id="tmpl_modal" type="text/html">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
|
@ -2249,113 +2068,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Music interface -->
|
||||
<script class="jsrender-template" id="tmpl_music_frame" type="text/html">
|
||||
<!-- First we want to define some variables if not defined yet. -->
|
||||
{{if !thumbnail}}
|
||||
{{*
|
||||
data.thumbnail = "img/music/no_thumbnail.svg";
|
||||
}}
|
||||
{{/if}}
|
||||
{{*
|
||||
if(!data.song_max_width) data.song_max_width = 300;
|
||||
|
||||
let width = calculate_width(data.song_name);
|
||||
if(width > data.song_max_width)
|
||||
data.song_use_scroller = true;
|
||||
}}
|
||||
|
||||
<div class="music-wrapper">
|
||||
<div class="container">
|
||||
<div class="left">
|
||||
<div class="static-card">
|
||||
<img src="{{:thumbnail}}" alt="Thumbnail" class="thumbnail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="controls hover">
|
||||
<label class="btn-forward"><span></span></label>
|
||||
<label class="btn-rewind"><span></span></label>
|
||||
<label class="btn-settings"><span></span></label>
|
||||
</div>
|
||||
<div class="flip-card">
|
||||
<img src="{{:thumbnail}}" alt="Thumbnail" class="thumbnail">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-overlay">
|
||||
<div class="timer">
|
||||
<div class="button-container">
|
||||
<div class="container-play-pause">
|
||||
<div class="button button_play">
|
||||
<svg x="0px" y="0px" viewBox="0 0 4.5 6.9"
|
||||
style="enable-background:new 0 0 4.5 6.9;">
|
||||
<defs>
|
||||
<filter id="shadow_play" title="Start replaying">
|
||||
<feDropShadow dx="4" dy="8" stdDeviation="4"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<polyline style="filter:url(#shadow_play);" class="button"
|
||||
points="0.6,0.3 3.9,3.4 0.6,6.6 "></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button_pause" title="Pause the current song">
|
||||
<svg x="0px" y="0px" viewBox="0 0 4.5 6.9"
|
||||
style="enable-background:new 0 0 4.5 6.9;">
|
||||
<defs>
|
||||
<filter id="shadow_pause">
|
||||
<feDropShadow dx="4" dy="8" stdDeviation="4"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g style="filter:url(#shadow_pause);">
|
||||
<line x1="0.4" y1="0.1" x2="0.4" y2="6.8"></line>
|
||||
<line x1="4.1" y1="0.1" x2="4.1" y2="6.8"></line>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="button button_stop" title="Stop the music bot">
|
||||
<svg x="0px" y="0px" viewBox="0 0 4.5 6.9"
|
||||
style="enable-background:new 0 0 4.5 6.9;">
|
||||
<defs>
|
||||
<filter id="shadow_stop">
|
||||
<feDropShadow dx="4" dy="8" stdDeviation="4"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g style="filter:url(#shadow_stop);">
|
||||
<rect x="0.25" y="1.45" width="4" height="4" fill="black"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="buffered"></div>
|
||||
<div class="played"></div>
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
<div class="time player_time">--:--</div>
|
||||
</div>
|
||||
<div class="song">
|
||||
{{if song_use_scroller}}
|
||||
<div class="scroll-left">
|
||||
<p class="name">{{>song_name}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="name" style="">{{>song_name}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script class="jsrender-template" id="tmpl_music_frame_empty" type="text/html">
|
||||
<div class="music-wrapper empty">
|
||||
<img src="img/music/empty_disk.svg">
|
||||
<a>{{tr "Not playing any music"/}}</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_poke_popup" type="text/html">
|
||||
<div class="container-poke">
|
||||
<div class="container-servers"></div>
|
||||
|
|
|
@ -40,7 +40,7 @@ import {PrivateConversationManager} from "tc-shared/conversations/PrivateConvers
|
|||
import {SelectedClientInfo} from "./SelectedClientInfo";
|
||||
import {SideBarManager} from "tc-shared/SideBarManager";
|
||||
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
|
||||
import {EventType} from "tc-shared/connectionlog/Definitions";
|
||||
import {PlaylistManager} from "tc-shared/music/PlaylistManager";
|
||||
|
||||
export enum InputHardwareState {
|
||||
MISSING,
|
||||
|
@ -157,6 +157,7 @@ export class ConnectionHandler {
|
|||
serverFeatures: ServerFeatures;
|
||||
|
||||
private sideBar: SideBarManager;
|
||||
private playlistManager: PlaylistManager;
|
||||
|
||||
private channelConversations: ChannelConversationManager;
|
||||
private privateConversations: PrivateConversationManager;
|
||||
|
@ -213,6 +214,7 @@ export class ConnectionHandler {
|
|||
this.channelTree = new ChannelTree(this);
|
||||
this.fileManager = new FileManager(this);
|
||||
this.permissions = new PermissionManager(this);
|
||||
this.playlistManager = new PlaylistManager(this);
|
||||
|
||||
this.sideBar = new SideBarManager(this);
|
||||
this.privateConversations = new PrivateConversationManager(this);
|
||||
|
@ -370,6 +372,10 @@ export class ConnectionHandler {
|
|||
return this.sideBar;
|
||||
}
|
||||
|
||||
getPlaylistManager() : PlaylistManager {
|
||||
return this.playlistManager;
|
||||
}
|
||||
|
||||
initializeLocalClient(clientId: number, acceptedName: string) {
|
||||
this._clientId = clientId;
|
||||
this.localClient["_clientId"] = clientId;
|
||||
|
@ -1056,6 +1062,9 @@ export class ConnectionHandler {
|
|||
this.sideBar?.destroy();
|
||||
this.sideBar = undefined;
|
||||
|
||||
this.playlistManager?.destroy();
|
||||
this.playlistManager = undefined;
|
||||
|
||||
this.clientInfoManager?.destroy();
|
||||
this.clientInfoManager = undefined;
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ export class ConnectionManager {
|
|||
private _container_channel_tree: JQuery;
|
||||
private _container_hostbanner: JQuery;
|
||||
private containerChannelVideo: ReplaceableContainer;
|
||||
private containerSideBar: HTMLDivElement;
|
||||
private containerFooter: HTMLDivElement;
|
||||
private containerServerLog: HTMLDivElement;
|
||||
|
||||
|
@ -157,6 +156,10 @@ export class ConnectionManager {
|
|||
all_connections() : ConnectionHandler[] {
|
||||
return this.connection_handlers;
|
||||
}
|
||||
|
||||
getSidebarController() : SideBarController {
|
||||
return this.sideBarController;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConnectionManagerEvents {
|
||||
|
|
|
@ -41,7 +41,7 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
this.single_command_handler = undefined;
|
||||
}
|
||||
|
||||
register_explicit_handler(command: string, callback: ExplicitCommandHandler) {
|
||||
register_explicit_handler(command: string, callback: ExplicitCommandHandler) : () => void {
|
||||
this.explicitHandlers[command] = this.explicitHandlers[command] || [];
|
||||
this.explicitHandlers[command].push(callback);
|
||||
|
||||
|
|
|
@ -77,11 +77,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
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(uniqueId, clientId, clientName) : EventClient | undefined {
|
||||
|
@ -446,11 +441,9 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
handleCommandChannelHide(json) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
const conversations = this.connection.client.getChannelConversations();
|
||||
|
||||
log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length);
|
||||
for(let index = 0; index < json.length; index++) {
|
||||
conversations.destroyConversation(parseInt(json[index]["cid"]));
|
||||
let channel = tree.findChannel(json[index]["cid"]);
|
||||
if(!channel) {
|
||||
logError(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)"));
|
||||
|
@ -471,26 +464,26 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
let client: ClientEntry;
|
||||
let channel = undefined;
|
||||
let old_channel = undefined;
|
||||
let reason_id, reason_msg;
|
||||
let reasonId, reasonMsg;
|
||||
|
||||
let invokerid, invokername, invokeruid;
|
||||
let invokerId, invokerName, invokerUniqueId;
|
||||
|
||||
for(const entry of json) {
|
||||
/* attempt to update properties if given */
|
||||
channel = typeof(entry["ctid"]) !== "undefined" ? tree.findChannel(parseInt(entry["ctid"])) : channel;
|
||||
old_channel = typeof(entry["cfid"]) !== "undefined" ? tree.findChannel(parseInt(entry["cfid"])) : old_channel;
|
||||
reason_id = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reason_id;
|
||||
reason_msg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reason_msg;
|
||||
reasonId = typeof(entry["reasonid"]) !== "undefined" ? entry["reasonid"] : reasonId;
|
||||
reasonMsg = typeof(entry["reason_msg"]) !== "undefined" ? entry["reason_msg"] : reasonMsg;
|
||||
|
||||
invokerid = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerid;
|
||||
invokername = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokername;
|
||||
invokeruid = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokeruid;
|
||||
invokerId = typeof(entry["invokerid"]) !== "undefined" ? parseInt(entry["invokerid"]) : invokerId;
|
||||
invokerName = typeof(entry["invokername"]) !== "undefined" ? entry["invokername"] : invokerName;
|
||||
invokerUniqueId = typeof(entry["invokeruid"]) !== "undefined" ? entry["invokeruid"] : invokerUniqueId;
|
||||
|
||||
client = tree.findClient(parseInt(entry["clid"]));
|
||||
|
||||
if(!client) {
|
||||
if(parseInt(entry["client_type_exact"]) == ClientType.CLIENT_MUSIC) {
|
||||
client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]);
|
||||
client = new MusicClientEntry(parseInt(entry["clid"]), entry["client_nickname"]) as any;
|
||||
} else {
|
||||
client = new ClientEntry(parseInt(entry["clid"]), entry["client_nickname"]);
|
||||
}
|
||||
|
@ -498,7 +491,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
/* TODO: Apply all other properties here as well and than register him */
|
||||
client.properties.client_unique_identifier = entry["client_unique_identifier"];
|
||||
client.properties.client_type = parseInt(entry["client_type"]);
|
||||
client = tree.insertClient(client, channel, { reason: reason_id, isServerJoin: parseInt(entry["cfid"]) === 0 });
|
||||
client = tree.insertClient(client, channel, { reason: reasonId, isServerJoin: parseInt(entry["cfid"]) === 0 });
|
||||
} else {
|
||||
tree.moveClient(client, channel);
|
||||
}
|
||||
|
@ -509,27 +502,27 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
channel_from: old_channel ? old_channel.log_data() : undefined,
|
||||
channel_to: channel ? channel.log_data() : undefined,
|
||||
client: client.log_data(),
|
||||
invoker: this.loggable_invoker(invokeruid, invokerid, invokername),
|
||||
message:reason_msg,
|
||||
reason: parseInt(reason_id),
|
||||
invoker: this.loggable_invoker(invokerUniqueId, invokerId, invokerName),
|
||||
message:reasonMsg,
|
||||
reason: parseInt(reasonId),
|
||||
});
|
||||
|
||||
if(reason_id == ViewReasonId.VREASON_USER_ACTION) {
|
||||
if(reasonId == ViewReasonId.VREASON_USER_ACTION) {
|
||||
if(own_channel == channel)
|
||||
if(old_channel)
|
||||
this.connection_handler.sound.play(Sound.USER_ENTERED);
|
||||
else
|
||||
this.connection_handler.sound.play(Sound.USER_ENTERED_CONNECT);
|
||||
} else if(reason_id == ViewReasonId.VREASON_MOVED) {
|
||||
} else if(reasonId == ViewReasonId.VREASON_MOVED) {
|
||||
if(own_channel == channel)
|
||||
this.connection_handler.sound.play(Sound.USER_ENTERED_MOVED);
|
||||
} else if(reason_id == ViewReasonId.VREASON_CHANNEL_KICK) {
|
||||
} else if(reasonId == ViewReasonId.VREASON_CHANNEL_KICK) {
|
||||
if(own_channel == channel)
|
||||
this.connection_handler.sound.play(Sound.USER_ENTERED_KICKED);
|
||||
} else if(reason_id == ViewReasonId.VREASON_SYSTEM) {
|
||||
} else if(reasonId == ViewReasonId.VREASON_SYSTEM) {
|
||||
|
||||
} else {
|
||||
console.warn(tr("Unknown reasonid for %o"), reason_id);
|
||||
console.warn(tr("Unknown reasonid for %o"), reasonId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -552,7 +545,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
client.updateVariables(...updates);
|
||||
|
||||
if(client instanceof LocalClientEntry) {
|
||||
client.initializeListener();
|
||||
this.connection_handler.update_voice_status();
|
||||
const conversations = this.connection.client.getChannelConversations();
|
||||
conversations.setSelectedConversation(conversations.findOrCreateConversation(client.currentChannel().channelId));
|
||||
|
@ -941,6 +933,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
key: string,
|
||||
value: string
|
||||
}[] = [];
|
||||
|
||||
for(let key in json) {
|
||||
if(key === "invokerid") continue;
|
||||
if(key === "invokername") continue;
|
||||
|
@ -1064,14 +1057,14 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
const bot_id = parseInt(json["bot_id"]);
|
||||
const client = this.connection.client.channelTree.find_client_by_dbid(bot_id);
|
||||
if(!client) {
|
||||
if(!client || !(client instanceof MusicClientEntry)) {
|
||||
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"])
|
||||
client.events.fire("notify_music_player_timestamp", {
|
||||
replayIndex: parseInt(json["player_replay_index"]),
|
||||
bufferedIndex: parseInt(json["player_buffered_index"])
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1080,7 +1073,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
const bot_id = parseInt(json["bot_id"]);
|
||||
const client = this.connection.client.channelTree.find_client_by_dbid(bot_id);
|
||||
if(!client) {
|
||||
if(!client || !(client instanceof MusicClientEntry)) {
|
||||
log.warn(LogCategory.CLIENT, tr("Received music bot status update for unknown bot (%d)"), bot_id);
|
||||
return;
|
||||
}
|
||||
|
@ -1092,80 +1085,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
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"]
|
||||
client.events.fire("notify_music_player_song_change", {
|
||||
newSong: song
|
||||
});
|
||||
}
|
||||
}
|
|
@ -174,5 +174,6 @@ export enum ErrorCode {
|
|||
|
||||
/** @deprecated Use SERVER_INSUFFICIENT_PERMISSIONS */
|
||||
PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS,
|
||||
/** @deprecated Use DATABASE_EMPTY_RESULT */
|
||||
EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {LaterPromise} from "../utils/LaterPromise";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
|
||||
export class CommandResult {
|
||||
success: boolean;
|
||||
|
@ -26,6 +27,9 @@ export class CommandResult {
|
|||
}
|
||||
|
||||
formattedMessage() {
|
||||
if(this.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
return "failed on permission " + this.json["failed_permid"]
|
||||
}
|
||||
return this.extra_message ? this.message + " (" + this.extra_message + ")" : this.message;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -382,6 +382,10 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
}
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_channel_deleted", event => {
|
||||
this.destroyConversation(event.channel.getChannelId());
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_client_enter_view", event => {
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
const targetConversation = this.findConversation(event.targetChannel.channelId);
|
||||
|
|
|
@ -73,8 +73,20 @@ export function tra(message: string, ...args: any[]) : JQuery[];
|
|||
export function tra(message: string, ...args: any[]) : any {
|
||||
message = /* @tr-ignore */ tr(message);
|
||||
for(const element of args) {
|
||||
if(typeof element !== "string" && typeof element !== "number" && typeof element !== "boolean")
|
||||
return formatMessage(message, ...args);
|
||||
if(element === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (typeof element) {
|
||||
case "boolean":
|
||||
case "number":
|
||||
case "string":
|
||||
case "undefined":
|
||||
continue;
|
||||
|
||||
default:
|
||||
return formatMessage(message, ...args);
|
||||
}
|
||||
}
|
||||
if(message.indexOf("{:") !== -1)
|
||||
return formatMessage(message, ...args);
|
||||
|
@ -329,7 +341,7 @@ export async function initialize() {
|
|||
declare global {
|
||||
interface Window {
|
||||
tr(message: string) : string;
|
||||
tra(message: string, ...args: (string | number | boolean)[]) : string;
|
||||
tra(message: string, ...args: (string | number | boolean | null | undefined)[]) : string;
|
||||
tra(message: string, ...args: any[]) : JQuery[];
|
||||
|
||||
log: any;
|
||||
|
|
|
@ -0,0 +1,555 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import _ = require("lodash");
|
||||
|
||||
export type PlaylistEntry = {
|
||||
type: "song",
|
||||
|
||||
id: number,
|
||||
previousId: number,
|
||||
|
||||
url: string,
|
||||
urlLoader: string,
|
||||
|
||||
invokerDatabaseId: number,
|
||||
metadata: PlaylistSongMetadata
|
||||
}
|
||||
|
||||
export type PlaylistSongMetadata = {
|
||||
status: "loading"
|
||||
} | {
|
||||
status: "unparsed",
|
||||
metadata: string
|
||||
} | {
|
||||
status: "loaded",
|
||||
metadata: string,
|
||||
|
||||
title: string,
|
||||
description: string,
|
||||
|
||||
thumbnailUrl?: string,
|
||||
|
||||
length: number,
|
||||
};
|
||||
|
||||
function parseUnparsedSongMetadata(metadata: string) : PlaylistSongMetadata {
|
||||
const meta = JSON.parse(metadata);
|
||||
|
||||
return {
|
||||
status: "loaded",
|
||||
metadata: metadata,
|
||||
|
||||
title: meta["title"],
|
||||
description: meta["description"],
|
||||
length: parseInt(meta["length"]),
|
||||
thumbnailUrl: !meta["thumbnail"] || meta["thumbnail"] === "none" ? undefined : meta["thumbnail"]
|
||||
};
|
||||
}
|
||||
|
||||
function parsePlayListSongEntry(data: any) {
|
||||
const result: PlaylistEntry = {
|
||||
type: "song",
|
||||
|
||||
id: parseInt(data["song_id"]),
|
||||
previousId: parseInt(data["song_previous_song_id"]),
|
||||
|
||||
url: data["song_url"],
|
||||
urlLoader: data["song_url_loader"],
|
||||
|
||||
invokerDatabaseId: parseInt(data["song_invoker"]),
|
||||
metadata: { status: "loading" }
|
||||
};
|
||||
|
||||
if(isNaN(result.id)) {
|
||||
throw tra("failed to parse song_id as an integer ({})", data["song_id"]);
|
||||
}
|
||||
|
||||
if(isNaN(result.previousId)) {
|
||||
throw tra("failed to parse song_previous_song_id as an integer ({})", data["song_previous_song_id"]);
|
||||
}
|
||||
|
||||
if(isNaN(result.invokerDatabaseId)) {
|
||||
throw tra("failed to parse song_invoker as an integer ({})", data["song_invoker"]);
|
||||
}
|
||||
|
||||
if(parseInt(data["song_loaded"]) === 1) {
|
||||
if(typeof data["song_metadata_title"] !== "undefined") {
|
||||
result.metadata = {
|
||||
status: "loaded",
|
||||
metadata: data["song_metadata"],
|
||||
|
||||
title: data["song_metadata_title"],
|
||||
description: data["song_metadata_description"],
|
||||
length: parseInt(data["song_metadata_length"]),
|
||||
thumbnailUrl: !data["song_metadata_thumbnail_url"] || data["song_metadata_thumbnail_url"] === "none" ? undefined : data["song_metadata_thumbnail_url"]
|
||||
};
|
||||
|
||||
if(isNaN(result.metadata.length)) {
|
||||
throw tra("failed to parse song_metadata_length as an integer ({})", data["song_metadata_length"]);
|
||||
}
|
||||
} else if(typeof data["song_metadata"] === "string") {
|
||||
try {
|
||||
result.metadata = parseUnparsedSongMetadata(data["song_metadata"]);
|
||||
} catch (_error) {
|
||||
result.metadata = {
|
||||
status: "unparsed",
|
||||
metadata: data["song_metadata"]
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw tr("Missing song metadata");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface SubscribedPlaylistEvents {
|
||||
notify_status_changed: {},
|
||||
|
||||
notify_entry_added: { entry: PlaylistEntry },
|
||||
notify_entry_deleted: { entry: PlaylistEntry },
|
||||
notify_entry_reordered: { entry: PlaylistEntry, oldPreviousId: number },
|
||||
notify_entry_updated: { entry: PlaylistEntry },
|
||||
}
|
||||
|
||||
export type SubscribedPlaylistStatus = {
|
||||
status: "loaded",
|
||||
songs: PlaylistEntry[]
|
||||
} | {
|
||||
status: "loading",
|
||||
} | {
|
||||
status: "error",
|
||||
error: string
|
||||
} | {
|
||||
status: "no-permissions",
|
||||
failedPermission: string
|
||||
} | {
|
||||
status: "unloaded"
|
||||
}
|
||||
|
||||
export abstract class SubscribedPlaylist {
|
||||
readonly events: Registry<SubscribedPlaylistEvents>;
|
||||
readonly playlistId: number;
|
||||
readonly serverUniqueId: string;
|
||||
|
||||
protected status: SubscribedPlaylistStatus;
|
||||
protected refCount: number;
|
||||
|
||||
protected constructor(serverUniqueId: string, playlistId: number) {
|
||||
this.refCount = 1;
|
||||
this.events = new Registry<SubscribedPlaylistEvents>();
|
||||
this.playlistId = playlistId;
|
||||
this.serverUniqueId = serverUniqueId;
|
||||
|
||||
this.status = { status: "unloaded" };
|
||||
}
|
||||
|
||||
ref() : SubscribedPlaylist {
|
||||
this.refCount++;
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
if(--this.refCount === 0) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the playlist songs from the remote server.
|
||||
* The playlist status will change on a successfully or failed query.
|
||||
*
|
||||
* @param forceQuery Forcibly query even we're subscribed and already aware of all songs.
|
||||
*/
|
||||
abstract async querySongs(forceQuery: boolean) : Promise<void>;
|
||||
|
||||
abstract async addSong(url: string, urlLoader: "any" | "youtube" | "ffmpeg" | "channel", targetSongId: number | 0, mode?: "before" | "after" | "last") : Promise<void>;
|
||||
abstract async deleteEntry(entryId: number) : Promise<void>;
|
||||
abstract async reorderEntry(entryId: number, targetEntryId: number, mode: "before" | "after") : Promise<void>;
|
||||
|
||||
getStatus() : Readonly<SubscribedPlaylistStatus> {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
protected setStatus(status: SubscribedPlaylistStatus) {
|
||||
if(_.isEqual(this.status, status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = status;
|
||||
this.events.fire("notify_status_changed");
|
||||
}
|
||||
}
|
||||
|
||||
class InternalSubscribedPlaylist extends SubscribedPlaylist {
|
||||
private readonly handle: PlaylistManager;
|
||||
private readonly listenerConnection: (() => void)[];
|
||||
private playlistSubscribed = false;
|
||||
|
||||
constructor(handle: PlaylistManager, playlistId: number) {
|
||||
super(handle.connection.getCurrentServerUniqueId(), playlistId);
|
||||
this.handle = handle;
|
||||
this.listenerConnection = [];
|
||||
|
||||
const serverConnection = this.handle.connection.serverConnection;
|
||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongadd", command => {
|
||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song add notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(playlistId !== this.playlistId || this.status.status !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
const song = parsePlayListSongEntry(command.arguments[0]);
|
||||
InternalSubscribedPlaylist.insertEntry(this.status.songs, song);
|
||||
|
||||
this.events.fire("notify_entry_added", {
|
||||
entry: song
|
||||
});
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongremove", command => {
|
||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song remove notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(playlistId !== this.playlistId || this.status.status !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
const songId = parseInt(command.arguments[0]["song_id"]);
|
||||
const song = this.removeEntry(this.status.songs, songId);
|
||||
|
||||
if(!song) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("notify_entry_deleted", {
|
||||
entry: song
|
||||
});
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongreorder", command => {
|
||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song reorder notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(playlistId !== this.playlistId || this.status.status !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryId = parseInt(command.arguments[0]["song_id"]);
|
||||
const previousEntryId = parseInt(command.arguments[0]["song_previous_song_id"]);
|
||||
|
||||
if(isNaN(entryId)) {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to parse song_id of playlist song reorder notify: %o"), command.arguments[0]["song_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isNaN(entryId)) {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to parse song_previous_song_id of playlist song reorder notify: %o"), command.arguments[0]["song_previous_song_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = this.removeEntry(this.status.songs, entryId);
|
||||
if(!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldOrderId = entry.previousId;
|
||||
entry.previousId = previousEntryId;
|
||||
InternalSubscribedPlaylist.insertEntry(this.status.songs, entry);
|
||||
|
||||
|
||||
this.events.fire("notify_entry_reordered", {
|
||||
entry: entry,
|
||||
oldPreviousId: oldOrderId
|
||||
});
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsongloaded", command => {
|
||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received a playlist song loaded notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(playlistId !== this.playlistId || this.status.status !== "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryId = parseInt(command.arguments[0]["song_id"]);
|
||||
const entry = this.status.songs.find(entry => entry.id === entryId);
|
||||
|
||||
const success = parseInt(command.arguments[0]["success"]) === 1;
|
||||
if(!success) {
|
||||
/* TODO: Change the entry type to failed and respect: load_error_msg */
|
||||
this.removeEntry(this.status.songs, entryId);
|
||||
this.events.fire("notify_entry_deleted", {
|
||||
entry: entry,
|
||||
});
|
||||
} else if(entry.metadata.status !== "loaded") {
|
||||
try {
|
||||
entry.metadata = parseUnparsedSongMetadata(command.arguments[0]["song_metadata"]);
|
||||
} catch (error) {
|
||||
entry.metadata = {
|
||||
status: "unparsed",
|
||||
metadata: command.arguments[0]["song_metadata"]
|
||||
};
|
||||
}
|
||||
|
||||
this.events.fire("notify_entry_updated", {
|
||||
entry: entry
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||
|
||||
if(this.handle["subscribedPlaylist"] === this) {
|
||||
this.handle["subscribedPlaylist"] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
handleUnsubscribed() {
|
||||
this.playlistSubscribed = false;
|
||||
this.setStatus({ status: "unloaded" });
|
||||
|
||||
if(this.handle["subscribedPlaylist"] === this) {
|
||||
this.handle["subscribedPlaylist"] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async querySongs(forceQuery: boolean) : Promise<void> {
|
||||
if(this.status.status === "loading") {
|
||||
return;
|
||||
} else if(this.status.status === "loaded" && !forceQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus({ status: "loading" });
|
||||
try {
|
||||
/* firstly subscribe to the playlist */
|
||||
if(!this.playlistSubscribed) {
|
||||
await this.handle.connection.serverConnection.send_command("playlistsetsubscription", { playlist_id: this.playlistId });
|
||||
if(this.handle["subscribedPlaylist"] !== this) {
|
||||
this.handle["subscribedPlaylist"]?.handleUnsubscribed();
|
||||
}
|
||||
this.handle["subscribedPlaylist"] = this;
|
||||
this.playlistSubscribed = true;
|
||||
}
|
||||
|
||||
/* now we can query the entries */
|
||||
const entries = await this.handle.queryPlaylistEntries(this.playlistId);
|
||||
/* TODO: Sort these entries! */
|
||||
this.setStatus({ status: "loaded", songs: entries });
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
this.setStatus({ status: "no-permissions", failedPermission: this.handle.connection.permissions.getFailedPermission(error) });
|
||||
return;
|
||||
} else if(error.id === ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
this.setStatus({ status: "loaded", songs: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.GENERAL, tr("Failed to query subscribed playlist entries: %o"), error);
|
||||
error = tr("Lookup the console for details");
|
||||
}
|
||||
|
||||
this.setStatus({ status: "error", error: error });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEntry(entryId: number): Promise<void> {
|
||||
await this.handle.removeSong(this.playlistId, entryId);
|
||||
}
|
||||
|
||||
private calculatePreviousSong(targetEntryId: number | undefined, mode: "before" | "after" | "last") : number {
|
||||
if(targetEntryId === 0) {
|
||||
return 0;
|
||||
} else if(mode === "before") {
|
||||
if(this.status.status !== "loaded") {
|
||||
throw tr("Invalid playlist state");
|
||||
}
|
||||
|
||||
const songIndex = this.status.songs.findIndex(song => song.id === targetEntryId);
|
||||
if(songIndex === -1) {
|
||||
throw tr("Invalid target id");
|
||||
}
|
||||
|
||||
return songIndex === 0 ? 0 : this.status.songs[songIndex - 1].id;
|
||||
} else if(mode === "last") {
|
||||
if(this.status.status !== "loaded") {
|
||||
throw tr("Invalid playlist state");
|
||||
}
|
||||
|
||||
return this.status.songs.last()?.id || 0;
|
||||
} else {
|
||||
return targetEntryId;
|
||||
}
|
||||
}
|
||||
|
||||
async reorderEntry(entryId: number, targetEntryId: number, mode: "before" | "after"): Promise<void> {
|
||||
await this.handle.reorderSong(this.playlistId, entryId, this.calculatePreviousSong(targetEntryId, mode));
|
||||
}
|
||||
|
||||
async addSong(url: string, urlLoader: "any" | "youtube" | "ffmpeg" | "channel", targetSongId: number | 0, mode?: "before" | "after" | "last"): Promise<void> {
|
||||
await this.handle.addSong(this.playlistId, url, urlLoader, this.calculatePreviousSong(targetSongId, mode || "after"));
|
||||
}
|
||||
|
||||
private static insertEntry(playlist: PlaylistEntry[], entry: PlaylistEntry) {
|
||||
const index = playlist.findIndex(e => e.id === entry.previousId);
|
||||
|
||||
const previousEntry = playlist[index];
|
||||
const nextEntry = playlist[index + 1];
|
||||
|
||||
playlist.splice(index + 1, 0, entry);
|
||||
entry.previousId = previousEntry ? previousEntry.id : 0;
|
||||
if(nextEntry) {
|
||||
nextEntry.previousId = entry.id;
|
||||
}
|
||||
}
|
||||
|
||||
private removeEntry(playlist: PlaylistEntry[], entryId: number) : PlaylistEntry | undefined {
|
||||
const index = playlist.findIndex(entry => entry.id === entryId);
|
||||
if(index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [ song ] = playlist.splice(index, 1);
|
||||
const previousEntry = playlist[index - 1];
|
||||
const nextEntry = playlist[index];
|
||||
|
||||
if(nextEntry) {
|
||||
nextEntry.previousId = previousEntry ? previousEntry.id : 0;
|
||||
}
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlaylistManager {
|
||||
readonly connection: ConnectionHandler;
|
||||
private listenerConnection: (() => void)[];
|
||||
|
||||
private playlistEntryListCache: {
|
||||
[key: number]: {
|
||||
result: PlaylistEntry[],
|
||||
promise: Promise<void>
|
||||
}
|
||||
} = {};
|
||||
|
||||
/* Use internally by InternalSubscribedPlaylist. Do not remove! */
|
||||
private subscribedPlaylist: InternalSubscribedPlaylist;
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
this.connection = connection;
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyplaylistsonglist", command => {
|
||||
const playlistId = parseInt(command.arguments[0]["playlist_id"]);
|
||||
if(isNaN(playlistId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received playlist song list notify with an invalid playlist id (%o)"), command.arguments[0]["playlist_id"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = this.playlistEntryListCache[playlistId];
|
||||
if(cache) {
|
||||
for(const entry of command.arguments) {
|
||||
if(!("song_id" in entry)) {
|
||||
/* Some TeaSpeak versions are sending empty bulks... */
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
cache.result.push(parsePlayListSongEntry(entry));
|
||||
} catch (error) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Failed to parse playlist entry: %o"), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||
|
||||
this.playlistEntryListCache = {};
|
||||
}
|
||||
|
||||
async queryPlaylistEntries(playlistId: number) : Promise<PlaylistEntry[]> {
|
||||
let cache = this.playlistEntryListCache[playlistId];
|
||||
if(!cache) {
|
||||
cache = this.playlistEntryListCache[playlistId] = {
|
||||
result: [],
|
||||
promise: (async () => {
|
||||
try {
|
||||
await this.connection.serverConnection.send_command("playlistsonglist", { "playlist_id": playlistId });
|
||||
} finally {
|
||||
delete this.playlistEntryListCache[playlistId];
|
||||
}
|
||||
})()
|
||||
};
|
||||
}
|
||||
|
||||
await cache.promise;
|
||||
return cache.result;
|
||||
}
|
||||
|
||||
async reorderSong(playlistId: number, songId: number, previousSongId: number) {
|
||||
await this.connection.serverConnection.send_command("playlistsongreorder", {
|
||||
"playlist_id": playlistId,
|
||||
"song_id": songId,
|
||||
"song_previous_song_id": previousSongId
|
||||
});
|
||||
}
|
||||
|
||||
async addSong(playlistId: number, url: string, urlLoader: "any" | "youtube" | "ffmpeg" | "channel", previousSongId: number | 0) {
|
||||
await this.connection.serverConnection.send_command("playlistsongadd", {
|
||||
"playlist_id": playlistId,
|
||||
"previous": previousSongId,
|
||||
"url": url,
|
||||
"type": urlLoader
|
||||
});
|
||||
}
|
||||
|
||||
async removeSong(playlistId: number, entryId: number) {
|
||||
await this.connection.serverConnection.send_command("playlistsongremove", {
|
||||
"playlist_id": playlistId,
|
||||
"song_id": entryId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param playlistId
|
||||
* @return Returns a subscribed playlist.
|
||||
* Attention: You have to manually destroy the object!
|
||||
*/
|
||||
createSubscribedPlaylist(playlistId: number) : SubscribedPlaylist {
|
||||
return new InternalSubscribedPlaylist(this, playlistId);
|
||||
}
|
||||
}
|
|
@ -156,20 +156,6 @@ export interface ClientEvents extends ChannelTreeEntryEvents {
|
|||
notify_status_icon_changed: { newIcon: ClientIcon },
|
||||
|
||||
notify_video_handle_changed: { oldHandle: VideoClient | undefined, newHandle: VideoClient | undefined },
|
||||
|
||||
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 },
|
||||
}
|
||||
|
||||
const StatusIconUpdateKeys: (keyof ClientProperties)[] = [
|
||||
|
@ -182,8 +168,8 @@ const StatusIconUpdateKeys: (keyof ClientProperties)[] = [
|
|||
"client_talk_power"
|
||||
];
|
||||
|
||||
export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||
readonly events: Registry<ClientEvents>;
|
||||
export class ClientEntry<Events extends ClientEvents = ClientEvents> extends ChannelTreeEntry<Events> {
|
||||
readonly events: Registry<Events>;
|
||||
channelTree: ChannelTree;
|
||||
|
||||
protected _clientId: number;
|
||||
|
@ -192,7 +178,6 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
protected _properties: ClientProperties;
|
||||
protected lastVariableUpdate: number = 0;
|
||||
protected _speaking: boolean;
|
||||
protected _listener_initialized: boolean;
|
||||
|
||||
protected voiceHandle: VoiceClient;
|
||||
protected voiceVolume: number;
|
||||
|
@ -211,7 +196,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
|
||||
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
|
||||
super();
|
||||
this.events = new Registry<ClientEvents>();
|
||||
this.events = new Registry<Events>();
|
||||
|
||||
this._properties = properties;
|
||||
this._properties.client_nickname = clientName;
|
||||
|
@ -365,24 +350,20 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
|
||||
this.events.fire("notify_mute_state_change", { muted: flagMuted });
|
||||
for(const client of this.channelTree.clients) {
|
||||
if(client === this || client.properties.client_unique_identifier !== this.properties.client_unique_identifier)
|
||||
if(client === this as any || client.properties.client_unique_identifier !== this.properties.client_unique_identifier) {
|
||||
continue;
|
||||
}
|
||||
client.setMuted(flagMuted, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeListener() {
|
||||
if(this._listener_initialized) return;
|
||||
this._listener_initialized = true;
|
||||
}
|
||||
|
||||
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||
return [
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"),
|
||||
callback: () => {
|
||||
this.channelTree.client.getSideBar().showClientInfo(this);
|
||||
this.channelTree.client.getSideBar().showClientInfo(this as any);
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
|
||||
|
@ -491,7 +472,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
}
|
||||
|
||||
open_assignment_modal() {
|
||||
createServerGroupAssignmentModal(this, (groups, flag) => {
|
||||
createServerGroupAssignmentModal(this as any, (groups, flag) => {
|
||||
if(groups.length == 0) return Promise.resolve(true);
|
||||
|
||||
if(groups.length == 1) {
|
||||
|
@ -519,8 +500,8 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
|
||||
open_text_chat() {
|
||||
const privateConversations = this.channelTree.client.getPrivateConversations();
|
||||
const conversation = privateConversations.findOrCreateConversation(this);
|
||||
conversation.setActiveClientEntry(this);
|
||||
const conversation = privateConversations.findOrCreateConversation(this as any);
|
||||
conversation.setActiveClientEntry(this as any);
|
||||
privateConversations.setSelectedConversation(conversation);
|
||||
|
||||
this.channelTree.client.getSideBar().showPrivateConversations();
|
||||
|
@ -559,7 +540,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: ClientIcon.About,
|
||||
name: tr("Show client info"),
|
||||
callback: () => openClientInfo(this)
|
||||
callback: () => openClientInfo(this as any)
|
||||
},
|
||||
contextmenu.Entry.HR(),
|
||||
{
|
||||
|
@ -687,13 +668,13 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: ClientIcon.Volume,
|
||||
name: tr("Change Volume"),
|
||||
callback: () => spawnClientVolumeChange(this)
|
||||
callback: () => spawnClientVolumeChange(this as any)
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Change playback latency"),
|
||||
callback: () => {
|
||||
spawnChangeLatency(this, this.voiceHandle.getLatencySettings(), () => {
|
||||
spawnChangeLatency(this as any, this.voiceHandle.getLatencySettings(), () => {
|
||||
this.voiceHandle.resetLatencySettings();
|
||||
return this.voiceHandle.getLatencySettings();
|
||||
}, settings => this.voiceHandle.setLatencySettings(settings), () => this.voiceHandle.flushBuffer());
|
||||
|
@ -987,10 +968,6 @@ export class LocalClientEntry extends ClientEntry {
|
|||
);
|
||||
}
|
||||
|
||||
initializeListener(): void {
|
||||
super.initializeListener();
|
||||
}
|
||||
|
||||
renameSelf(new_name: string) : Promise<boolean> {
|
||||
const old_name = this.properties.client_nickname;
|
||||
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
|
||||
|
@ -1038,10 +1015,10 @@ export enum MusicClientPlayerState {
|
|||
}
|
||||
|
||||
export class MusicClientProperties extends ClientProperties {
|
||||
player_state: number = 0; /* MusicClientPlayerState */
|
||||
player_state: number = 0;
|
||||
player_volume: number = 0;
|
||||
|
||||
client_playlist_id: number = 0;
|
||||
client_playlist_id: number = -1;
|
||||
client_disabled: boolean = false;
|
||||
|
||||
client_flag_notify_song_change: boolean = false;
|
||||
|
@ -1075,7 +1052,19 @@ export class MusicClientPlayerInfo extends SongInfo {
|
|||
player_description: string = "";
|
||||
}
|
||||
|
||||
export class MusicClientEntry extends ClientEntry {
|
||||
export interface MusicClientEvents extends ClientEvents {
|
||||
notify_music_player_song_change: { newSong: SongInfo | undefined },
|
||||
notify_music_player_timestamp: {
|
||||
bufferedIndex: number,
|
||||
replayIndex: number
|
||||
},
|
||||
|
||||
notify_subscribe_state_changed: { subscribed: boolean },
|
||||
}
|
||||
|
||||
export class MusicClientEntry extends ClientEntry<MusicClientEvents> {
|
||||
private subscribed: boolean;
|
||||
|
||||
private _info_promise: Promise<MusicClientPlayerInfo>;
|
||||
private _info_promise_age: number = 0;
|
||||
private _info_promise_resolve: any;
|
||||
|
@ -1083,6 +1072,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
|
||||
constructor(clientId, clientName) {
|
||||
super(clientId, clientName, new MusicClientProperties());
|
||||
this.subscribed = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -1096,6 +1086,30 @@ export class MusicClientEntry extends ClientEntry {
|
|||
return this._properties as MusicClientProperties;
|
||||
}
|
||||
|
||||
isSubscribed() : boolean {
|
||||
return this.subscribed;
|
||||
}
|
||||
|
||||
async subscribe() : Promise<void> {
|
||||
if(this.subscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.channelTree.client.serverConnection.send_command("musicbotsetsubscription", { bot_id: this.properties.client_database_id });
|
||||
|
||||
this.channelTree.clients.forEach(client => {
|
||||
if(client instanceof MusicClientEntry) {
|
||||
if(client.subscribed) {
|
||||
client.subscribed = false;
|
||||
client.events.fire("notify_subscribe_state_changed", { subscribed: false });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.subscribed = true;
|
||||
this.events.fire("notify_subscribe_state_changed", { subscribed: this.subscribed });
|
||||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
|
||||
let trigger_close = true;
|
||||
contextmenu.spawn_context_menu(x, y,
|
||||
|
@ -1198,7 +1212,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-volume",
|
||||
name: tr("Change local volume"),
|
||||
callback: () => spawnClientVolumeChange(this)
|
||||
callback: () => spawnClientVolumeChange(this as any)
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -1217,7 +1231,7 @@ export class MusicClientEntry extends ClientEntry {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Change playback latency"),
|
||||
callback: () => {
|
||||
spawnChangeLatency(this, this.voiceHandle.getLatencySettings(), () => {
|
||||
spawnChangeLatency(this as any, this.voiceHandle.getLatencySettings(), () => {
|
||||
this.voiceHandle.resetLatencySettings();
|
||||
return this.voiceHandle.getLatencySettings();
|
||||
}, settings => this.voiceHandle.setLatencySettings(settings), () => this.voiceHandle.flushBuffer());
|
||||
|
@ -1245,10 +1259,6 @@ export class MusicClientEntry extends ClientEntry {
|
|||
);
|
||||
}
|
||||
|
||||
initializeListener(): void {
|
||||
super.initializeListener();
|
||||
}
|
||||
|
||||
handlePlayerInfo(json) {
|
||||
if(json) {
|
||||
const info = new MusicClientPlayerInfo();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {ConnectionHandler} from "../../ConnectionHandler";
|
||||
import {ChannelConversationController} from "./side/ChannelConversationController";
|
||||
import {PrivateConversationController} from "./side/PrivateConversationController";
|
||||
import {ClientInfoController} from "tc-shared/ui/frames/side/ClientInfoController";
|
||||
import {SideHeaderController} from "tc-shared/ui/frames/side/HeaderController";
|
||||
|
@ -10,6 +9,7 @@ import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {ChannelBarController} from "tc-shared/ui/frames/side/ChannelBarController";
|
||||
import {MusicBotController} from "tc-shared/ui/frames/side/MusicBotController";
|
||||
|
||||
export class SideBarController {
|
||||
private readonly uiEvents: Registry<SideBarEvents>;
|
||||
|
@ -21,6 +21,7 @@ export class SideBarController {
|
|||
private clientInfo: ClientInfoController;
|
||||
private privateConversations: PrivateConversationController;
|
||||
private channelBar: ChannelBarController;
|
||||
private musicPanel: MusicBotController;
|
||||
|
||||
constructor() {
|
||||
this.listenerConnection = [];
|
||||
|
@ -29,6 +30,7 @@ export class SideBarController {
|
|||
this.uiEvents.on("query_content", () => this.sendContent());
|
||||
this.uiEvents.on("query_content_data", event => this.sendContentData(event.content));
|
||||
|
||||
this.musicPanel = new MusicBotController();
|
||||
this.channelBar = new ChannelBarController();
|
||||
this.privateConversations = new PrivateConversationController();
|
||||
this.clientInfo = new ClientInfoController();
|
||||
|
@ -48,6 +50,7 @@ export class SideBarController {
|
|||
this.clientInfo.setConnectionHandler(connection);
|
||||
this.privateConversations.setConnectionHandler(connection);
|
||||
this.channelBar.setConnectionHandler(connection);
|
||||
this.musicPanel.setConnection(connection);
|
||||
|
||||
if(connection) {
|
||||
this.listenerConnection.push(connection.getSideBar().events.on("notify_content_type_changed", () => this.sendContent()));
|
||||
|
@ -60,6 +63,9 @@ export class SideBarController {
|
|||
this.header?.destroy();
|
||||
this.header = undefined;
|
||||
|
||||
this.musicPanel?.destroy();
|
||||
this.musicPanel = undefined;
|
||||
|
||||
this.channelBar?.destroy();
|
||||
this.channelBar = undefined;
|
||||
|
||||
|
@ -77,6 +83,10 @@ export class SideBarController {
|
|||
}), container);
|
||||
}
|
||||
|
||||
getMusicController() : MusicBotController {
|
||||
return this.musicPanel;
|
||||
}
|
||||
|
||||
private sendContent() {
|
||||
if(this.currentConnection) {
|
||||
this.uiEvents.fire("notify_content", { content: this.currentConnection.getSideBar().getSideBarContent() });
|
||||
|
@ -145,7 +155,10 @@ export class SideBarController {
|
|||
|
||||
this.uiEvents.fire_react("notify_content_data", {
|
||||
content: "music-manage",
|
||||
data: { }
|
||||
data: {
|
||||
botEvents: this.musicPanel.getBotUiEvents(),
|
||||
playlistEvents: this.musicPanel.getPlaylistUiEvents()
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConve
|
|||
import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||
import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import {ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
|
||||
import {MusicBotUiEvents} from "tc-shared/ui/frames/side/MusicBotDefinitions";
|
||||
import {MusicPlaylistUiEvents} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions";
|
||||
|
||||
/* TODO: Somehow outsource the event registries to IPC? */
|
||||
|
||||
|
@ -20,7 +22,8 @@ export interface SideBarTypeData {
|
|||
events: Registry<ClientInfoEvents>,
|
||||
},
|
||||
"music-manage": {
|
||||
|
||||
botEvents: Registry<MusicBotUiEvents>,
|
||||
playlistEvents: Registry<MusicPlaylistUiEvents>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer";
|
|||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import React = require("react");
|
||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
import {MusicBotRenderer} from "tc-shared/ui/frames/side/MusicBotRenderer";
|
||||
|
||||
const cssStyle = require("./SideBarRenderer.scss");
|
||||
|
||||
|
@ -60,6 +61,18 @@ const ContentRendererClientInfo = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const ContentRendererMusicManage = () => {
|
||||
const contentData = useContentData("music-manage");
|
||||
if(!contentData) { return null; }
|
||||
|
||||
return (
|
||||
<MusicBotRenderer
|
||||
botEvents={contentData.botEvents}
|
||||
playlistEvents={contentData.playlistEvents}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SideBarFrame = (props: { type: SideBarType }) => {
|
||||
switch (props.type) {
|
||||
case "channel":
|
||||
|
@ -84,7 +97,11 @@ const SideBarFrame = (props: { type: SideBarType }) => {
|
|||
);
|
||||
|
||||
case "music-manage":
|
||||
/* TODO! */
|
||||
return (
|
||||
<ErrorBoundary key={props.type}>
|
||||
<ContentRendererMusicManage />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
case "none":
|
||||
default:
|
||||
|
|
|
@ -2,7 +2,10 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|||
import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
|
||||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import {LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
|
||||
import {openMusicManage} from "tc-shared/ui/modal/ModalMusicManage";
|
||||
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
|
||||
const ChannelInfoUpdateProperties: (keyof ChannelProperties)[] = [
|
||||
"channel_name",
|
||||
|
@ -55,18 +58,31 @@ export class SideHeaderController {
|
|||
});
|
||||
|
||||
this.uiEvents.on("action_bot_manage", () => {
|
||||
/* FIXME: TODO! */
|
||||
/*
|
||||
const bot = this.connection.getSideBar().music_info().current_bot();
|
||||
if(!bot) return;
|
||||
const client = this.connection.channelTree.getSelectedEntry();
|
||||
if(!(client instanceof MusicClientEntry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
openMusicManage(this.connection, bot);
|
||||
*/
|
||||
openMusicManage(this.connection, client);
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_bot_add_song", () => {
|
||||
/* FIXME: TODO! */
|
||||
//this.connection.side_bar.music_info().events.fire("action_song_add")
|
||||
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) return;
|
||||
|
||||
server_connections.getSidebarController().getMusicController().getPlaylistUiEvents()
|
||||
.fire("action_add_song", {
|
||||
url: result as string,
|
||||
mode: "last"
|
||||
});
|
||||
}).open();
|
||||
});
|
||||
|
||||
this.uiEvents.on("query_client_info_own_client", () => this.sendClientInfoOwnClient());
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
MusicBotPlayerState,
|
||||
MusicBotPlayerTimestamp,
|
||||
MusicBotUiEvents
|
||||
} from "tc-shared/ui/frames/side/MusicBotDefinitions";
|
||||
import {MusicPlaylistController} from "tc-shared/ui/frames/side/MusicPlaylistController";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {MusicClientEntry, MusicClientPlayerState, SongInfo} from "tc-shared/tree/Client";
|
||||
import {SubscribedPlaylist} from "tc-shared/music/PlaylistManager";
|
||||
import {MusicPlaylistUiEvents} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
|
||||
export class MusicBotController {
|
||||
private readonly uiEvents: Registry<MusicBotUiEvents>;
|
||||
private readonly playlistController: MusicPlaylistController;
|
||||
|
||||
private listenerConnection: (() => void)[];
|
||||
private listenerBot: (() => void)[];
|
||||
|
||||
private currentConnection: ConnectionHandler;
|
||||
private currentBot: MusicClientEntry;
|
||||
|
||||
private playerTimestamp: MusicBotPlayerTimestamp;
|
||||
private currentSongInfo: SongInfo;
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<MusicBotUiEvents>();
|
||||
this.playlistController = new MusicPlaylistController();
|
||||
|
||||
this.uiEvents.on("query_player_state", () => this.reportPlayerState());
|
||||
this.uiEvents.on("query_song_info", () => this.reportSongInfo());
|
||||
this.uiEvents.on("query_volume", event => this.reportVolume(event.mode));
|
||||
|
||||
this.uiEvents.on("action_player_action", event => {
|
||||
if(!this.currentConnection) { return; }
|
||||
|
||||
let playerAction: number;
|
||||
switch (event.action) {
|
||||
case "play":
|
||||
playerAction = 1;
|
||||
break;
|
||||
|
||||
case "pause":
|
||||
playerAction = 2;
|
||||
break;
|
||||
|
||||
case "forward":
|
||||
playerAction = 3;
|
||||
break;
|
||||
|
||||
case "rewind":
|
||||
playerAction = 4;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentConnection.serverConnection.send_command("musicbotplayeraction", {
|
||||
bot_id: this.currentBot.properties.client_database_id,
|
||||
action: playerAction
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
logError(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.uiEvents.on("action_seek_to", event => {
|
||||
if(!this.playerTimestamp || !this.playerTimestamp.base || !this.playerTimestamp.seekable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timePassed = Date.now() - this.playerTimestamp.base;
|
||||
const offset = event.target - timePassed - this.playerTimestamp.playOffset;
|
||||
this.currentConnection.serverConnection.send_command("musicbotplayeraction", {
|
||||
bot_id: this.currentBot.properties.client_database_id,
|
||||
action: offset > 0 ? 5 : 6,
|
||||
units: Math.floor(Math.abs(offset))
|
||||
}).catch(error => {
|
||||
this.reportPlayerTimestamp();
|
||||
if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
logError(LogCategory.CLIENT, tr("Failed to perform action %s on bot: %o"), event.type, error);
|
||||
createErrorModal(tr("Failed to change replay offset."), tr("Failed to change replay offset for music bot.")).open();
|
||||
});
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_change_volume", event => {
|
||||
if(!this.currentBot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.mode === "local") {
|
||||
this.currentBot.setAudioVolume(event.volume);
|
||||
} else {
|
||||
this.currentConnection.serverConnection.send_command("clientedit", {
|
||||
clid: this.currentBot.clientId(),
|
||||
player_volume: event.volume
|
||||
}).catch(() => {
|
||||
this.reportVolume("remote");
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
this.playlistController.uiEvents.on("action_select_entry", event => {
|
||||
if(event.entryId === (this.currentSongInfo?.song_id || 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.currentConnection || !this.currentBot) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentConnection.serverConnection.send_command("playlistsongsetcurrent", {
|
||||
playlist_id: this.currentBot.properties.client_playlist_id,
|
||||
song_id: event.entryId
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) return;
|
||||
|
||||
logError(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();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.playlistController.destroy();
|
||||
|
||||
this.uiEvents.destroy();
|
||||
|
||||
this.listenerBot?.forEach(callback => callback());
|
||||
this.listenerBot = [];
|
||||
this.currentBot = undefined;
|
||||
|
||||
this.listenerConnection?.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
this.currentConnection = undefined;
|
||||
|
||||
this.currentSongInfo = undefined;
|
||||
this.playerTimestamp = undefined;
|
||||
}
|
||||
|
||||
getBotUiEvents() : Registry<MusicBotUiEvents> {
|
||||
return this.uiEvents;
|
||||
}
|
||||
|
||||
getPlaylistUiEvents() : Registry<MusicPlaylistUiEvents> {
|
||||
return this.playlistController.uiEvents;
|
||||
}
|
||||
|
||||
setConnection(connection: ConnectionHandler) {
|
||||
if(this.currentConnection === connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerConnection?.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.currentConnection = connection;
|
||||
if(this.currentConnection) {
|
||||
this.initializeConnectionListener(connection);
|
||||
const entry = connection.channelTree.getSelectedEntry();
|
||||
if(entry instanceof MusicClientEntry) {
|
||||
this.setBot(entry);
|
||||
} else {
|
||||
this.setBot(undefined);
|
||||
}
|
||||
} else {
|
||||
this.setBot(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
setBot(bot: MusicClientEntry) {
|
||||
if(this.currentBot === bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerBot?.forEach(callback => callback());
|
||||
this.listenerBot = [];
|
||||
|
||||
this.currentBot = bot;
|
||||
if(this.currentBot) {
|
||||
this.initializeBotListener(bot);
|
||||
/* client_playlist_id is a non in view variable */
|
||||
bot.updateClientVariables().then(undefined);
|
||||
bot.subscribe().then(undefined); /* TODO: Error handling */
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_bot_changed");
|
||||
|
||||
this.currentSongInfo = undefined;
|
||||
this.reportSongInfo();
|
||||
|
||||
this.playerTimestamp = undefined;
|
||||
this.reportPlayerTimestamp();
|
||||
|
||||
this.reportVolume("local");
|
||||
this.reportVolume("remote");
|
||||
|
||||
this.updatePlaylist();
|
||||
this.updatePlayerInfo().then(undefined);
|
||||
}
|
||||
|
||||
private initializeConnectionListener(connection: ConnectionHandler) {
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_selected_entry_changed", event => {
|
||||
if(event.newEntry instanceof MusicClientEntry) {
|
||||
this.setBot(event.newEntry);
|
||||
} else {
|
||||
this.setBot(undefined);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private initializeBotListener(bot: MusicClientEntry) {
|
||||
this.listenerBot.push(bot.events.on("notify_properties_updated", event => {
|
||||
if("client_playlist_id" in event.updated_properties) {
|
||||
this.updatePlaylist();
|
||||
}
|
||||
|
||||
if("player_state" in event.updated_properties) {
|
||||
this.reportPlayerState();
|
||||
|
||||
if(bot.properties.player_state === MusicClientPlayerState.PLAYING && !this.currentSongInfo?.song_loaded) {
|
||||
/* We don't receive song loaded updates... */
|
||||
this.updatePlayerInfo().then(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if("player_volume" in event.updated_properties) {
|
||||
this.reportVolume("remote");
|
||||
}
|
||||
}));
|
||||
|
||||
this.listenerBot.push(bot.events.on("notify_music_player_song_change", event => {
|
||||
this.playerTimestamp = undefined;
|
||||
this.reportPlayerTimestamp();
|
||||
|
||||
this.currentSongInfo = event.newSong;
|
||||
this.reportSongInfo();
|
||||
|
||||
this.playlistController.setCurrentSongId(this.currentSongInfo?.song_id || 0);
|
||||
}));
|
||||
|
||||
this.listenerBot.push(bot.events.on("notify_music_player_timestamp", event => {
|
||||
if(!this.playerTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playerTimestamp = Object.assign({}, this.playerTimestamp);
|
||||
this.playerTimestamp.base = Date.now();
|
||||
this.playerTimestamp.playOffset = event.replayIndex;
|
||||
this.playerTimestamp.bufferOffset = event.bufferedIndex;
|
||||
this.reportPlayerTimestamp();
|
||||
}));
|
||||
|
||||
this.listenerBot.push(bot.events.on("notify_audio_level_changed", () => {
|
||||
this.reportVolume("local");
|
||||
}));
|
||||
|
||||
/* TODO: Handle bot unsubscribed event */
|
||||
}
|
||||
|
||||
private updatePlaylist() {
|
||||
let playlistId: number = 0;
|
||||
if(this.currentConnection && this.currentBot) {
|
||||
playlistId = this.currentBot.properties.client_playlist_id;
|
||||
console.error("Client playlist id: %o", playlistId);
|
||||
}
|
||||
|
||||
let playlist: SubscribedPlaylist;
|
||||
if(playlistId > 0) {
|
||||
const currentPlaylist = this.playlistController.getCurrentPlaylist();
|
||||
if(typeof currentPlaylist === "object" && currentPlaylist.playlistId === playlistId) {
|
||||
return;
|
||||
}
|
||||
|
||||
playlist = this.currentConnection.getPlaylistManager().createSubscribedPlaylist(playlistId);
|
||||
}
|
||||
|
||||
this.playlistController.setCurrentPlaylist(playlistId === -1 ? "loading" : playlist);
|
||||
playlist?.unref();
|
||||
}
|
||||
|
||||
private async updatePlayerInfo() {
|
||||
if(this.currentBot) {
|
||||
try {
|
||||
const playerInfo = await this.currentBot.requestPlayerInfo();
|
||||
|
||||
if(playerInfo.song_id > 0) {
|
||||
this.currentSongInfo = playerInfo;
|
||||
if(playerInfo.song_loaded) {
|
||||
this.playerTimestamp = {
|
||||
base: Date.now(),
|
||||
total: playerInfo.player_max_index,
|
||||
playOffset: playerInfo.player_replay_index,
|
||||
bufferOffset: playerInfo.player_buffered_index,
|
||||
seekable: playerInfo.player_seekable
|
||||
}
|
||||
} else {
|
||||
this.playerTimestamp = undefined;
|
||||
}
|
||||
} else {
|
||||
this.currentSongInfo = undefined;
|
||||
this.playerTimestamp = undefined;
|
||||
}
|
||||
|
||||
this.playlistController.setCurrentSongId(this.currentSongInfo?.song_id || 0);
|
||||
} catch (error) {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to request music bot player info: %o"), error);
|
||||
this.currentSongInfo = undefined;
|
||||
}
|
||||
} else {
|
||||
this.currentSongInfo = undefined;
|
||||
}
|
||||
|
||||
this.reportSongInfo();
|
||||
this.reportPlayerTimestamp();
|
||||
/* TODO: Report timestamp etc */
|
||||
}
|
||||
|
||||
private reportPlayerState() {
|
||||
let state: MusicBotPlayerState = "paused";
|
||||
|
||||
if(this.currentBot) {
|
||||
state = this.currentBot.isCurrentlyPlaying() ? "playing" : "paused";
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_player_state", { state: state });
|
||||
}
|
||||
|
||||
private reportSongInfo() {
|
||||
if(this.currentSongInfo && this.currentBot) {
|
||||
const playerState = this.currentBot.properties.player_state;
|
||||
if(playerState === MusicClientPlayerState.SLEEPING || playerState === MusicClientPlayerState.STOPPED) {
|
||||
this.uiEvents.fire_react("notify_song_info", { info: { type: "none" }});
|
||||
} else {
|
||||
if(this.currentSongInfo.song_loaded) {
|
||||
this.uiEvents.fire_react("notify_song_info", {
|
||||
info: {
|
||||
type: "song",
|
||||
url: this.currentSongInfo.song_url,
|
||||
description: this.currentSongInfo.song_description,
|
||||
title: this.currentSongInfo.song_title,
|
||||
thumbnail: this.currentSongInfo.song_thumbnail
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.uiEvents.fire_react("notify_song_info", { info: { type: "loading", url: this.currentSongInfo.song_url }});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.uiEvents.fire_react("notify_song_info", { info: { type: "none" }});
|
||||
}
|
||||
}
|
||||
|
||||
private reportPlayerTimestamp() {
|
||||
this.uiEvents.fire_react("notify_player_timestamp", {
|
||||
timestamp: this.playerTimestamp || {
|
||||
base: 0,
|
||||
bufferOffset: 0,
|
||||
playOffset: 0,
|
||||
total: 0,
|
||||
seekable: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private reportVolume(mode: "local" | "remote") {
|
||||
this.uiEvents.fire_react("notify_volume", {
|
||||
mode: mode,
|
||||
volume: this.currentBot ? mode === "local" ? this.currentBot.getAudioVolume() : this.currentBot.properties.player_volume : 0
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
export type MusicBotSongInfo = {
|
||||
type: "none"
|
||||
} | {
|
||||
type: "loading",
|
||||
url: string
|
||||
} | {
|
||||
type: "song",
|
||||
|
||||
url: string,
|
||||
thumbnail: string,
|
||||
|
||||
title: string,
|
||||
description: string
|
||||
};
|
||||
|
||||
export type MusicBotPlayerState = "paused" | "playing";
|
||||
|
||||
export type MusicBotPlayerTimestamp = {
|
||||
base: number,
|
||||
playOffset: number,
|
||||
bufferOffset: number,
|
||||
total: number,
|
||||
seekable: boolean
|
||||
};
|
||||
|
||||
export interface MusicBotUiEvents {
|
||||
action_player_action: {
|
||||
action: "play" | "pause" | "forward" | "rewind"
|
||||
},
|
||||
action_seek_to: {
|
||||
target: number
|
||||
},
|
||||
action_change_volume: {
|
||||
mode: "local" | "remote",
|
||||
volume: number
|
||||
}
|
||||
|
||||
query_player_state: {},
|
||||
query_player_timestamp: {},
|
||||
query_song_info: {},
|
||||
query_volume: {
|
||||
mode: "local" | "remote",
|
||||
}
|
||||
|
||||
notify_player_state: {
|
||||
state: MusicBotPlayerState
|
||||
},
|
||||
notify_player_timestamp: {
|
||||
timestamp: MusicBotPlayerTimestamp
|
||||
},
|
||||
notify_player_seek_timestamp: {
|
||||
offset: number | undefined,
|
||||
applySeek: boolean
|
||||
},
|
||||
notify_song_info: {
|
||||
info: MusicBotSongInfo
|
||||
},
|
||||
notify_volume: {
|
||||
mode: "local" | "remote",
|
||||
volume: number
|
||||
},
|
||||
notify_bot_changed: {}
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
$color_client_normal: #cccccc;
|
||||
$client_info_avatar_size: 10em;
|
||||
$bot_thumbnail_width: 16em;
|
||||
$bot_thumbnail_height: 9em;
|
||||
|
||||
.bodySeek {
|
||||
* {
|
||||
@include user-select(none!important);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
padding-bottom: .5em;
|
||||
|
||||
height: 100%;
|
||||
|
||||
.playlist {
|
||||
margin-top: 1.5em;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.player {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.containerThumbnail {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: inline-block;
|
||||
margin: calc(#{$bot_thumbnail_height} / -2) .75em .5em .5em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
border-radius: .5em;
|
||||
overflow: hidden;
|
||||
|
||||
.thumbnail {
|
||||
overflow: hidden;
|
||||
|
||||
width: $bot_thumbnail_width;
|
||||
height: $bot_thumbnail_height;
|
||||
|
||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerTimeline {
|
||||
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;
|
||||
}
|
||||
|
||||
.buffered {
|
||||
background-color: #2f3133;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controlButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.controlButton {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerSongInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
min-width: 1em;
|
||||
|
||||
.songName {
|
||||
font-size: 1.5em;
|
||||
|
||||
min-width: 1em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
align-self: center;
|
||||
color: #999999;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.songDescription {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.containerControl {
|
||||
position: relative;
|
||||
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.volumeOverlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
max-width: 100%;
|
||||
width: 4em;
|
||||
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
border-radius: .2em;
|
||||
|
||||
@include transition(all .2s ease-in-out);
|
||||
|
||||
&.expended {
|
||||
width: 100%;
|
||||
background-color: #2e2e2e;
|
||||
|
||||
margin-top: -3em;
|
||||
margin-bottom: -1em;
|
||||
|
||||
padding-top: 1.25em;
|
||||
padding-bottom: 1.25em;
|
||||
|
||||
box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
|
||||
.content {
|
||||
min-width: 4em;
|
||||
|
||||
width: 100%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #6c6c60!important;
|
||||
|
||||
&:hover {
|
||||
fill: #59594f !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controlButton {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
margin-left: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.containerSlider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.name {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
width: 9em;
|
||||
color: var(--text);
|
||||
|
||||
a {
|
||||
width: 2.3em;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,450 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {MusicPlaylistUiEvents} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions";
|
||||
import {DefaultThumbnail, formatPlaytime, MusicPlaylistList} from "tc-shared/ui/frames/side/MusicPlaylistRenderer";
|
||||
import * as React from "react";
|
||||
import {
|
||||
MusicBotPlayerState,
|
||||
MusicBotPlayerTimestamp,
|
||||
MusicBotSongInfo,
|
||||
MusicBotUiEvents
|
||||
} from "tc-shared/ui/frames/side/MusicBotDefinitions";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {preview_image} from "tc-shared/ui/frames/image_preview";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
|
||||
const cssStyle = require("./MusicBotRenderer.scss");
|
||||
|
||||
const EventContext = React.createContext<Registry<MusicBotUiEvents>>(undefined);
|
||||
const SongInfoContext = React.createContext<MusicBotSongInfo>(undefined);
|
||||
const TimestampContext = React.createContext<MusicBotPlayerTimestamp & { seekOffset: number | undefined }>(undefined);
|
||||
|
||||
const ButtonRewind = () => (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" xmlSpace="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>
|
||||
);
|
||||
|
||||
const ButtonPlay = () => (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" xmlSpace="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>
|
||||
);
|
||||
|
||||
const ButtonPause = () => (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" xmlSpace="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>
|
||||
);
|
||||
|
||||
const ButtonForward = () => (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" xmlSpace="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>
|
||||
);
|
||||
|
||||
const ButtonVolume = () => (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="93.038px" height="93.038px" viewBox="0 0 93.038 93.038"
|
||||
xmlSpace="preserve">
|
||||
<path d="M46.547,75.521c0,1.639-0.947,3.128-2.429,3.823c-0.573,0.271-1.187,0.402-1.797,0.402c-0.966,0-1.923-0.332-2.696-0.973
|
||||
l-23.098-19.14H4.225C1.892,59.635,0,57.742,0,55.409V38.576c0-2.334,1.892-4.226,4.225-4.226h12.303l23.098-19.14
|
||||
c1.262-1.046,3.012-1.269,4.493-0.569c1.481,0.695,2.429,2.185,2.429,3.823L46.547,75.521L46.547,75.521z M62.784,68.919
|
||||
c-0.103,0.007-0.202,0.011-0.304,0.011c-1.116,0-2.192-0.441-2.987-1.237l-0.565-0.567c-1.482-1.479-1.656-3.822-0.408-5.504
|
||||
c3.164-4.266,4.834-9.323,4.834-14.628c0-5.706-1.896-11.058-5.484-15.478c-1.366-1.68-1.24-4.12,0.291-5.65l0.564-0.565
|
||||
c0.844-0.844,1.975-1.304,3.199-1.231c1.192,0.06,2.305,0.621,3.061,1.545c4.977,6.09,7.606,13.484,7.606,21.38
|
||||
c0,7.354-2.325,14.354-6.725,20.24C65.131,68.216,64.007,68.832,62.784,68.919z M80.252,81.976
|
||||
c-0.764,0.903-1.869,1.445-3.052,1.495c-0.058,0.002-0.117,0.004-0.177,0.004c-1.119,0-2.193-0.442-2.988-1.237l-0.555-0.555
|
||||
c-1.551-1.55-1.656-4.029-0.246-5.707c6.814-8.104,10.568-18.396,10.568-28.982c0-11.011-4.019-21.611-11.314-29.847
|
||||
c-1.479-1.672-1.404-4.203,0.17-5.783l0.554-0.555c0.822-0.826,1.89-1.281,3.115-1.242c1.163,0.033,2.263,0.547,3.036,1.417
|
||||
c8.818,9.928,13.675,22.718,13.675,36.01C93.04,59.783,88.499,72.207,80.252,81.976z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SongInfoProvider = (props) => {
|
||||
const events = useContext(EventContext);
|
||||
const [ info, setInfo ] = useState<MusicBotSongInfo>(() => {
|
||||
events.fire("query_song_info");
|
||||
return { type: "none" };
|
||||
});
|
||||
events.reactUse("notify_song_info", event => setInfo(event.info));
|
||||
|
||||
return (
|
||||
<SongInfoContext.Provider value={info}>
|
||||
{props.children}
|
||||
</SongInfoContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const PlayerTimestampProvider = (props) => {
|
||||
const events = useContext(EventContext);
|
||||
const [ timestamp, setTimestamp ] = useState<MusicBotPlayerTimestamp>(() => {
|
||||
events.fire("query_player_timestamp");
|
||||
return {
|
||||
seekable: false,
|
||||
bufferOffset: 0,
|
||||
playOffset: 0,
|
||||
base: 0,
|
||||
total: 0,
|
||||
};
|
||||
});
|
||||
const [ seekOffset, setSeekOffset ] = useState<number | undefined>(undefined);
|
||||
|
||||
events.reactUse("notify_player_timestamp", event => setTimestamp(event.timestamp), undefined, []);
|
||||
events.reactUse("notify_player_seek_timestamp", event => {
|
||||
if(event.applySeek && timestamp.base > 0 && typeof seekOffset === "number") {
|
||||
timestamp.playOffset = seekOffset;
|
||||
timestamp.bufferOffset += Date.now() - timestamp.base;
|
||||
timestamp.base = Date.now();
|
||||
}
|
||||
setSeekOffset(event.offset);
|
||||
}, undefined, [ seekOffset, timestamp ]);
|
||||
|
||||
return (
|
||||
<TimestampContext.Provider value={Object.assign({ seekOffset: seekOffset }, timestamp)}>
|
||||
{props.children}
|
||||
</TimestampContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const Thumbnail = React.memo(() => {
|
||||
const info = useContext(SongInfoContext);
|
||||
|
||||
let thumbnail;
|
||||
switch (info.type) {
|
||||
case "none":
|
||||
thumbnail = <DefaultThumbnail type={"none-present"} key={"none"} />;
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
thumbnail = <DefaultThumbnail type={"loading"} key={"loading"} />;
|
||||
break;
|
||||
|
||||
case "song":
|
||||
if(info.thumbnail) {
|
||||
thumbnail = (
|
||||
<img
|
||||
draggable={false}
|
||||
key={"song-thumbnail"}
|
||||
src={info.thumbnail}
|
||||
onClick={() => preview_image(info.thumbnail, info.thumbnail)}
|
||||
alt={tr("Thumbnail")}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
thumbnail = <DefaultThumbnail type={"none-present"} key={"none"} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div className={cssStyle.thumbnail}>
|
||||
{thumbnail}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Timestamps = () => {
|
||||
const info = useContext(TimestampContext);
|
||||
|
||||
const [ revision, setRevision ] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => setRevision(revision + 1), 990);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
let current: number;
|
||||
if(info.seekable && typeof info.seekOffset === "number") {
|
||||
current = info.seekOffset;
|
||||
} else {
|
||||
const timePassed = info.base > 0 ? Date.now() - info.base : 0;
|
||||
current = info.playOffset + timePassed;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.timestamps}>
|
||||
<div>{formatPlaytime(current)}</div>
|
||||
<div>{formatPlaytime(info.total)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Timeline = () => {
|
||||
const events = useContext(EventContext);
|
||||
const info = useContext(TimestampContext);
|
||||
const refContainer = useRef<HTMLDivElement>();
|
||||
const [ moveActive, setMoveActive ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if(!moveActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.classList.add(cssStyle.bodySeek);
|
||||
|
||||
let currentSeekOffset;
|
||||
const mouseMoveListener = (event: MouseEvent) => {
|
||||
if(!refContainer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, width } = refContainer.current.getBoundingClientRect();
|
||||
if(event.pageX <= x) {
|
||||
events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = 0, applySeek: false });
|
||||
} else if(event.pageX >= x + width) {
|
||||
events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = info.total, applySeek: false });
|
||||
} else {
|
||||
events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = Math.floor((event.pageX - x) / width * info.total), applySeek: false });
|
||||
}
|
||||
};
|
||||
|
||||
const mouseUpListener = () => {
|
||||
if(typeof currentSeekOffset === "number") {
|
||||
events.fire("action_seek_to", { target: currentSeekOffset });
|
||||
}
|
||||
events.fire("notify_player_seek_timestamp", { offset: undefined, applySeek: true });
|
||||
setMoveActive(false);
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", mouseMoveListener);
|
||||
document.addEventListener("mouseleave", mouseUpListener);
|
||||
document.addEventListener("mouseup", mouseUpListener);
|
||||
document.addEventListener("focusout", mouseUpListener);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove(cssStyle.bodySeek);
|
||||
|
||||
document.removeEventListener("mousemove", mouseMoveListener);
|
||||
document.removeEventListener("mouseleave", mouseUpListener);
|
||||
document.removeEventListener("mouseup", mouseUpListener);
|
||||
document.removeEventListener("focusout", mouseUpListener);
|
||||
};
|
||||
}, [ moveActive ]);
|
||||
|
||||
let current: number, buffered: number;
|
||||
|
||||
const timePassed = info.base > 0 ? Date.now() - info.base : 0;
|
||||
if(info.seekable && typeof info.seekOffset === "number") {
|
||||
current = info.seekOffset;
|
||||
} else {
|
||||
current = info.playOffset + timePassed;
|
||||
}
|
||||
buffered = info.bufferOffset + timePassed;
|
||||
|
||||
let widthBuffered = info.total === 0 ? 100 : (buffered / info.total) * 100;
|
||||
let widthCurrent = info.total === 0 ? 100 : (current / info.total) * 100;
|
||||
|
||||
return (
|
||||
<div className={cssStyle.timeline} ref={refContainer}>
|
||||
<div className={cssStyle.indicator + " " + cssStyle.buffered} style={{ width: widthBuffered.toFixed(2) + "%"}} />
|
||||
<div className={cssStyle.indicator + " " + cssStyle.playtime} style={{ width: widthCurrent.toFixed(2) + "%"}} />
|
||||
<div
|
||||
className={cssStyle.thumb}
|
||||
style={{ marginLeft: widthCurrent.toFixed(2) + "%"}}
|
||||
onMouseDown={() => info.seekable && info.total > 0 && setMoveActive(true)}
|
||||
>
|
||||
<div className={cssStyle.dot} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SongInfo = () => {
|
||||
const info = useContext(SongInfoContext);
|
||||
|
||||
let name, nameTitle/*, description */;
|
||||
switch (info.type) {
|
||||
case "none":
|
||||
name = <Translatable key={"no-song"}>No song selected</Translatable>;
|
||||
break;
|
||||
|
||||
case "song":
|
||||
name = info.title || info.url;
|
||||
nameTitle = name;
|
||||
/* description = info.description; */
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
name = info.url;
|
||||
nameTitle = name;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerSongInfo}>
|
||||
<a className={cssStyle.songName} title={nameTitle}>{name}</a>
|
||||
{/* <a className={cssStyle.songDescription}>{description}</a> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ControlButtons = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [ playerState, setPlayerState ] = useState<MusicBotPlayerState>(() => {
|
||||
events.fire("query_player_state");
|
||||
return "paused";
|
||||
});
|
||||
events.reactUse("notify_player_state", event => setPlayerState(event.state));
|
||||
|
||||
let playButton;
|
||||
if(playerState === "paused") {
|
||||
playButton = (
|
||||
<div className={cssStyle.controlButton} key={"play"} onClick={() => events.fire("action_player_action", { action: "play" })}>
|
||||
<ButtonPlay />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
playButton = (
|
||||
<div className={cssStyle.controlButton} key={"pause"} onClick={() => events.fire("action_player_action", { action: "pause" })}>
|
||||
<ButtonPause />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.controlButtons}>
|
||||
<div className={cssStyle.controlButton} onClick={() => events.fire("action_player_action", { action: "rewind" })}>
|
||||
<ButtonRewind />
|
||||
</div>
|
||||
{playButton}
|
||||
<div className={cssStyle.controlButton} onClick={() => events.fire("action_player_action", { action: "forward" })}>
|
||||
<ButtonForward />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VolumeSlider = (props: { mode: "local" | "remote", }) => {
|
||||
const events = useContext(EventContext);
|
||||
const refSlider = useRef<Slider>();
|
||||
|
||||
const [ value, setValue ] = useState(() => {
|
||||
events.fire("query_volume", { mode: props.mode });
|
||||
return 100;
|
||||
});
|
||||
events.reactUse("notify_volume", event => {
|
||||
if(event.mode !== props.mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(event.volume * 100);
|
||||
if(!refSlider.current?.state.active) {
|
||||
refSlider.current?.setState({ value: event.volume * 100 });
|
||||
}
|
||||
});
|
||||
|
||||
let name;
|
||||
if(props.mode === "local") {
|
||||
name = <Translatable key={"local"}>Local</Translatable>;
|
||||
} else {
|
||||
name = <Translatable key={"remote"}>Remote</Translatable>;
|
||||
}
|
||||
|
||||
const valueString = (value: number) => {
|
||||
if(value > 100) {
|
||||
return "+" + (value - 100).toFixed(0);
|
||||
} else if(value == 100) {
|
||||
return "±0";
|
||||
} else {
|
||||
return "-" + (100 - value).toFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerSlider}>
|
||||
<div className={cssStyle.name}>{name} (<a>{valueString(value)}</a>%):</div>
|
||||
<Slider
|
||||
ref={refSlider}
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
stepSize={1}
|
||||
value={value}
|
||||
className={cssStyle.slider}
|
||||
onInput={value => {
|
||||
setValue(value);
|
||||
if(props.mode === "local") {
|
||||
events.fire("action_change_volume", { mode: props.mode, volume: value / 100 });
|
||||
}
|
||||
}}
|
||||
onChange={value => {
|
||||
setValue(value);
|
||||
events.fire("action_change_volume", { mode: props.mode, volume: value / 100 });
|
||||
}}
|
||||
tooltip={value => valueString(value) + "%"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VolumeSetting = () => {
|
||||
const events = useContext(EventContext);
|
||||
const [ expended, setExpended ] = useState(false);
|
||||
events.reactUse("notify_bot_changed", () => setExpended(false));
|
||||
|
||||
return (
|
||||
<div className={cssStyle.volumeOverlay + " " + (expended ? cssStyle.expended : "")}>
|
||||
<div className={cssStyle.controlButton} onClick={() => setExpended(!expended)}>
|
||||
<ButtonVolume />
|
||||
</div>
|
||||
<div className={cssStyle.content}>
|
||||
<VolumeSlider mode={"local"} />
|
||||
<VolumeSlider mode={"remote"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MusicBotPlayer = () => {
|
||||
return (
|
||||
<div className={cssStyle.player}>
|
||||
<SongInfoProvider>
|
||||
<div className={cssStyle.containerThumbnail}>
|
||||
<Thumbnail />
|
||||
</div>
|
||||
<SongInfo />
|
||||
</SongInfoProvider>
|
||||
<PlayerTimestampProvider>
|
||||
<div className={cssStyle.containerTimeline}>
|
||||
<Timestamps />
|
||||
<Timeline />
|
||||
</div>
|
||||
</PlayerTimestampProvider>
|
||||
<div className={cssStyle.containerControl}>
|
||||
<ControlButtons />
|
||||
<VolumeSetting />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const MusicBotRenderer = (props: {
|
||||
botEvents: Registry<MusicBotUiEvents>,
|
||||
playlistEvents: Registry<MusicPlaylistUiEvents>
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={props.botEvents}>
|
||||
<div className={cssStyle.container} draggable={false}>
|
||||
<MusicBotPlayer />
|
||||
<MusicPlaylistList events={props.playlistEvents} className={cssStyle.playlist} />
|
||||
</div>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
import {SubscribedPlaylist} from "tc-shared/music/PlaylistManager";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {MusicPlaylistStatus, MusicPlaylistUiEvents} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
|
||||
export class MusicPlaylistController {
|
||||
readonly uiEvents: Registry<MusicPlaylistUiEvents>;
|
||||
|
||||
private currentPlaylist: SubscribedPlaylist | "loading";
|
||||
private listenerPlaylist: (() => void)[];
|
||||
|
||||
private currentSongId: number;
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<MusicPlaylistUiEvents>();
|
||||
|
||||
this.uiEvents.on("query_playlist_status", () => this.reportPlaylistStatus());
|
||||
this.uiEvents.on("query_entry_status", event => this.reportPlaylistEntry(event.entryId));
|
||||
|
||||
this.uiEvents.on("action_load_playlist", event => {
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
this.currentPlaylist.querySongs(event.forced).then(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_entry_delete", async event => {
|
||||
try {
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
await this.currentPlaylist.deleteEntry(event.entryId);
|
||||
} else {
|
||||
throw tr("No playlist selected");
|
||||
}
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to delete playlist song entry: %o"), error);
|
||||
error = tr("Lookup the console for details");
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to delete song"), tra("Failed to delete song:\n", error)).open();
|
||||
}
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_reorder_song", async event => {
|
||||
try {
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
await this.currentPlaylist.reorderEntry(event.entryId, event.targetEntryId, event.mode);
|
||||
} else {
|
||||
throw tr("No playlist selected");
|
||||
}
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to reorder playlist song entry: %o"), error);
|
||||
error = tr("Lookup the console for details");
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to reorder song"), tra("Failed to reorder song:\n", error)).open();
|
||||
}
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_add_song", async event => {
|
||||
try {
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
await this.currentPlaylist.addSong(event.url, "any", event.targetEntryId, event.mode);
|
||||
} else {
|
||||
throw tr("No playlist selected");
|
||||
}
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to add song to playlist entry: %o"), error);
|
||||
error = tr("Lookup the console for details");
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to add song song"), tra("Failed to add song:\n", error)).open();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
|
||||
setCurrentPlaylist(playlist: SubscribedPlaylist | "loading") {
|
||||
if(this.currentPlaylist === playlist) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerPlaylist?.forEach(callback => callback());
|
||||
this.listenerPlaylist = [];
|
||||
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
this.currentPlaylist.unref();
|
||||
}
|
||||
this.currentPlaylist = playlist;
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
this.currentPlaylist.ref();
|
||||
this.initializePlaylistListener(this.currentPlaylist);
|
||||
}
|
||||
|
||||
this.reportPlaylistStatus();
|
||||
}
|
||||
|
||||
getCurrentPlaylist() : SubscribedPlaylist | "loading" | undefined {
|
||||
return this.currentPlaylist;
|
||||
}
|
||||
|
||||
getCurrentSongId() : number {
|
||||
return this.currentSongId;
|
||||
}
|
||||
|
||||
setCurrentSongId(id: number | 0) {
|
||||
if(this.currentSongId === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentSongId = id;
|
||||
this.reportPlaylistStatus();
|
||||
}
|
||||
|
||||
private initializePlaylistListener(playlist: SubscribedPlaylist) {
|
||||
this.listenerPlaylist.push(playlist.events.on("notify_status_changed", () => this.reportPlaylistStatus()));
|
||||
|
||||
this.listenerPlaylist.push(playlist.events.on("notify_entry_added", () => this.reportPlaylistStatus()));
|
||||
this.listenerPlaylist.push(playlist.events.on("notify_entry_reordered", () => this.reportPlaylistStatus()));
|
||||
this.listenerPlaylist.push(playlist.events.on("notify_entry_deleted", () => this.reportPlaylistStatus()));
|
||||
|
||||
this.listenerPlaylist.push(playlist.events.on("notify_entry_updated", event => this.reportPlaylistEntry(event.entry.id)));
|
||||
}
|
||||
|
||||
private reportPlaylistStatus() {
|
||||
let status: MusicPlaylistStatus = { status: "unselected" };
|
||||
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
const playlistStatus = this.currentPlaylist.getStatus();
|
||||
switch (playlistStatus.status) {
|
||||
case "unloaded":
|
||||
/* just query the playlist status instead of letting the user manually do this */
|
||||
this.currentPlaylist.querySongs(false).then(undefined);
|
||||
return;
|
||||
|
||||
case "loading":
|
||||
status = { status: "loading" };
|
||||
break;
|
||||
|
||||
case "loaded":
|
||||
status = {
|
||||
status: "loaded",
|
||||
/* Drag and drop only supports lowercase characters! */
|
||||
serverUniqueId: this.currentPlaylist.serverUniqueId.toLowerCase(),
|
||||
playlistId: this.currentPlaylist.playlistId,
|
||||
songs: playlistStatus.songs.map(song => song.id),
|
||||
activeSong: this.currentSongId
|
||||
};
|
||||
break;
|
||||
|
||||
case "no-permissions":
|
||||
status = {
|
||||
status: "no-permissions",
|
||||
failedPermission: playlistStatus.failedPermission
|
||||
};
|
||||
break;
|
||||
|
||||
case "error":
|
||||
status = {
|
||||
status: "error",
|
||||
reason: playlistStatus.error
|
||||
};
|
||||
break;
|
||||
}
|
||||
} else if(this.currentPlaylist === "loading") {
|
||||
status = { status: "loading" };
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_playlist_status", { status: status });
|
||||
}
|
||||
|
||||
private reportPlaylistEntry(entryId: number) {
|
||||
if(typeof this.currentPlaylist === "object") {
|
||||
const playlistStatus = this.currentPlaylist.getStatus();
|
||||
if(playlistStatus.status === "loaded") {
|
||||
const song = playlistStatus.songs.find(song => song.id === entryId);
|
||||
if(song) {
|
||||
if(song.metadata.status === "loaded") {
|
||||
this.uiEvents.fire_react("notify_entry_status", {
|
||||
entryId: entryId,
|
||||
status: {
|
||||
type: "song",
|
||||
url: song.url,
|
||||
thumbnailImage: song.metadata.thumbnailUrl,
|
||||
title: song.metadata.title,
|
||||
description: song.metadata.description,
|
||||
length: song.metadata.length
|
||||
}
|
||||
});
|
||||
} else if(song.metadata.status === "unparsed") {
|
||||
this.uiEvents.fire_react("notify_entry_status", {
|
||||
entryId: entryId,
|
||||
status: {
|
||||
type: "song",
|
||||
|
||||
url: song.url,
|
||||
thumbnailImage: undefined,
|
||||
title: song.url,
|
||||
description: "",
|
||||
length: 0
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.uiEvents.fire_react("notify_entry_status", {
|
||||
entryId: entryId,
|
||||
status: { type: "loading", url: song.url }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: May fire an error? */
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
export type MusicPlaylistStatus = {
|
||||
status: "loading" | "unselected" | "unloaded"
|
||||
} | {
|
||||
status: "error",
|
||||
reason: string
|
||||
} | {
|
||||
status: "loaded",
|
||||
|
||||
serverUniqueId: string,
|
||||
playlistId: number,
|
||||
|
||||
songs: number[],
|
||||
activeSong: number
|
||||
} | {
|
||||
status: "no-permissions",
|
||||
failedPermission: string
|
||||
};
|
||||
|
||||
export type MusicPlaylistEntryInfo = {
|
||||
type: "loading",
|
||||
url: string | undefined
|
||||
} | {
|
||||
type: "song",
|
||||
|
||||
url: string,
|
||||
|
||||
title: string,
|
||||
description: string,
|
||||
|
||||
length: number,
|
||||
thumbnailImage: string
|
||||
}
|
||||
|
||||
export interface MusicPlaylistUiEvents {
|
||||
action_load_playlist: { forced: boolean },
|
||||
action_entry_delete: { entryId: number },
|
||||
action_reorder_song: { entryId: number, targetEntryId: number, mode: "before" | "after" },
|
||||
action_add_song: { url: string, mode: "before" | "after" | "last", targetEntryId?: number },
|
||||
action_select_entry: { entryId: number },
|
||||
|
||||
query_playlist_status: {},
|
||||
query_entry_status: { entryId: number }
|
||||
|
||||
notify_playlist_status: {
|
||||
status: MusicPlaylistStatus
|
||||
},
|
||||
notify_entry_status: {
|
||||
entryId: number,
|
||||
status: MusicPlaylistEntryInfo
|
||||
},
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
$color_client_normal: #cccccc;
|
||||
$client_info_avatar_size: 10em;
|
||||
$bot_thumbnail_width: 16em;
|
||||
$bot_thumbnail_height: 9em;
|
||||
|
||||
.containerPlaylist {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: calc(3em + 4px);
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
code {
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 8em;
|
||||
font-size: .8em;
|
||||
align-self: center;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.error {
|
||||
/* TODO: Text color */
|
||||
}
|
||||
}
|
||||
|
||||
.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();
|
||||
|
||||
.reorderIndicator {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$playlist_entry_height: 3.7em;
|
||||
.playlistEntry {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
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: $playlist_entry_height;
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
|
||||
.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();
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.currentSong {
|
||||
background-color: hsla(130, 50%, 30%, .25);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(130, 50%, 50%, .25);
|
||||
}
|
||||
|
||||
.delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.insertMarkerAbove {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
|
||||
border-top: 2px solid var(--channel-tree-move-border);
|
||||
}
|
||||
}
|
||||
|
||||
&.insertMarkerBellow {
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
|
||||
border-bottom: 2px solid var(--channel-tree-move-border);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
MusicPlaylistEntryInfo,
|
||||
MusicPlaylistStatus,
|
||||
MusicPlaylistUiEvents
|
||||
} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions";
|
||||
import {useContext, useRef, useState} from "react";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {preview_image} from "tc-shared/ui/frames/image_preview";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
|
||||
const cssStyle = require("./MusicPlaylistRenderer.scss");
|
||||
|
||||
const EventContext = React.createContext<Registry<MusicPlaylistUiEvents>>(undefined);
|
||||
const kPlaylistDragPrefixIds = "x-teaspeak-playlist-drag-ids-";
|
||||
const kPlaylistDragSongUrl = "x-teaspeak-playlist-drag-url";
|
||||
|
||||
function parseDragIds(transfer: DataTransfer) : { serverUniqueId: string, entryId: number, playlistId: number } | undefined {
|
||||
for(const item of transfer.items) {
|
||||
if(!item.type.startsWith(kPlaylistDragPrefixIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ handlerId, playlistIdStr, entryIdStr ] = item.type.substring(kPlaylistDragPrefixIds.length).split("-");
|
||||
return { serverUniqueId: handlerId, entryId: parseInt(entryIdStr), playlistId: parseInt(playlistIdStr) };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatPlaytime(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);
|
||||
}
|
||||
|
||||
export const DefaultThumbnail = (_props: { type: "loading" | "none-present" }) => {
|
||||
return (
|
||||
<img
|
||||
draggable={false}
|
||||
src="img/music/no-thumbnail.png"
|
||||
alt={useTr("loading")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const PlaylistEntry = React.memo((props: { serverUniqueId: string, playlistId: number, entryId: number, active: boolean }) => {
|
||||
const events = useContext(EventContext);
|
||||
const refContainer = useRef<HTMLDivElement>();
|
||||
const refDragLeaveTimer = useRef<number>();
|
||||
|
||||
const [ insertMarker, setInsertMarker ] = useState<"above" | "bellow" | "none">("none");
|
||||
|
||||
const [ status, setStatus ] = useState<MusicPlaylistEntryInfo>(() => {
|
||||
events.fire("query_entry_status", { entryId: props.entryId });
|
||||
return { type: "loading", url: undefined };
|
||||
});
|
||||
events.reactUse("notify_entry_status", event => event.entryId === props.entryId && setStatus(event.status));
|
||||
|
||||
let thumbnail, firstRow: React.ReactElement | string = "", secondRow: React.ReactElement | string = "", secondRowTitle, length;
|
||||
switch (status.type) {
|
||||
case "song":
|
||||
if(status.thumbnailImage) {
|
||||
thumbnail = (
|
||||
<img
|
||||
draggable={false}
|
||||
key={"song-thumbnail"}
|
||||
src={status.thumbnailImage}
|
||||
onClick={() => preview_image(status.thumbnailImage, status.thumbnailImage)}
|
||||
alt={useTr("Thumbnail")}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
thumbnail = <DefaultThumbnail key={"default-none"} type={"none-present"} />;
|
||||
}
|
||||
firstRow = status.title;
|
||||
|
||||
const description = status.description || tr("No song description given.");
|
||||
secondRow = description.substr(0, 100);
|
||||
secondRowTitle = description;
|
||||
|
||||
length = formatPlaytime(status.length);
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
if(status.url) {
|
||||
secondRow = status.url;
|
||||
}
|
||||
|
||||
/* fall through expected */
|
||||
default:
|
||||
thumbnail = <DefaultThumbnail key={"default"} type={"loading"} />;
|
||||
firstRow = <React.Fragment key={"loading"}><Translatable>Loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||
break;
|
||||
}
|
||||
|
||||
let insertClass;
|
||||
switch (insertMarker) {
|
||||
case "above":
|
||||
insertClass = cssStyle.insertMarkerAbove;
|
||||
break;
|
||||
|
||||
case "bellow":
|
||||
insertClass = cssStyle.insertMarkerBellow;
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
//cssStyle.playlistEntry + " " + cssStyle.shown + " " + (props.active ? cssStyle.currentSong : "")
|
||||
return (
|
||||
<div
|
||||
ref={refContainer}
|
||||
className={joinClassList(cssStyle.playlistEntry, cssStyle.shown, props.active && cssStyle.currentSong, insertClass)}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
|
||||
spawnContextMenu({ pageY: event.pageY, pageX: event.pageX }, [
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Copy URL"),
|
||||
click: () => { status.type === "song" ? copy_to_clipboard(status.url) : undefined; },
|
||||
visible: status.type === "song"
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Copy description"),
|
||||
click: () => { status.type === "song" ? copy_to_clipboard(status.description) : undefined; },
|
||||
visible: status.type === "song" && !!status.description
|
||||
},
|
||||
{
|
||||
type: "normal",
|
||||
label: tr("Remove song"),
|
||||
click: () => events.fire("action_entry_delete", { entryId: props.entryId })
|
||||
}
|
||||
]);
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => {
|
||||
event.dataTransfer.setData(kPlaylistDragPrefixIds + props.serverUniqueId + "-" + props.playlistId + "-" + props.entryId, "");
|
||||
if(status.type === "song") {
|
||||
event.dataTransfer.setData(kPlaylistDragSongUrl, status.url);
|
||||
}
|
||||
event.dataTransfer.effectAllowed = "all";
|
||||
}}
|
||||
onDragOver={event => {
|
||||
const info = parseDragIds(event.dataTransfer);
|
||||
if(!info || !refContainer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if(info.playlistId === props.playlistId && info.serverUniqueId === props.serverUniqueId) {
|
||||
if(info.entryId === props.entryId) {
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
} else if([...event.dataTransfer.items].findIndex(item => item.type === kPlaylistDragSongUrl) !== -1) {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
} else {
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if(refDragLeaveTimer.current) {
|
||||
clearTimeout(refDragLeaveTimer.current);
|
||||
refDragLeaveTimer.current = undefined;
|
||||
}
|
||||
|
||||
const containerRect = refContainer.current.getBoundingClientRect();
|
||||
switch (insertMarker) {
|
||||
case "bellow": {
|
||||
const yThreshold = containerRect.y + containerRect.height * .4;
|
||||
if(event.pageY < yThreshold) {
|
||||
setInsertMarker("above");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "above": {
|
||||
const yThreshold = containerRect.y + containerRect.height * .6;
|
||||
if(event.pageY > yThreshold) {
|
||||
setInsertMarker("bellow");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "none": {
|
||||
const yThreshold = containerRect.y + containerRect.height / 2;
|
||||
if(event.pageY > yThreshold) {
|
||||
setInsertMarker("bellow");
|
||||
} else {
|
||||
setInsertMarker("above");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if(refDragLeaveTimer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* The drag leave event might also gets fired when the component itself updates. If set set the insert marker to none it might cause flickering */
|
||||
refDragLeaveTimer.current = setTimeout(() => {
|
||||
setInsertMarker("none");
|
||||
refDragLeaveTimer.current = undefined;
|
||||
}, 50);
|
||||
}}
|
||||
onDragExit={() => setInsertMarker("none")}
|
||||
onDragEnd={() => setInsertMarker("none")}
|
||||
onDrop={event => {
|
||||
const info = parseDragIds(event.dataTransfer);
|
||||
console.error("Info: %o - %o - %o", info, insertMarker, { playlistId: props.playlistId, serverUniqueId: props.serverUniqueId });
|
||||
if(!info) {
|
||||
setInsertMarker("none");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.playlistId === props.playlistId && info.serverUniqueId === props.serverUniqueId) {
|
||||
switch (insertMarker) {
|
||||
case "above":
|
||||
events.fire("action_reorder_song", { entryId: info.entryId, targetEntryId: props.entryId, mode: "before" });
|
||||
break;
|
||||
|
||||
case "bellow":
|
||||
events.fire("action_reorder_song", { entryId: info.entryId, targetEntryId: props.entryId, mode: "after" });
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const songUrl = event.dataTransfer.getData(kPlaylistDragSongUrl);
|
||||
if(!songUrl) { return; }
|
||||
|
||||
switch (insertMarker) {
|
||||
case "above":
|
||||
events.fire("action_add_song", { targetEntryId: props.entryId, mode: "before", url: songUrl });
|
||||
break;
|
||||
|
||||
case "bellow":
|
||||
events.fire("action_add_song", { targetEntryId: props.entryId, mode: "after", url: songUrl });
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setInsertMarker("none");
|
||||
}}
|
||||
onDoubleClick={() => events.fire("action_select_entry", { entryId: props.entryId })}
|
||||
>
|
||||
<div className={cssStyle.thumbnail}>
|
||||
{thumbnail}
|
||||
</div>
|
||||
<div className={cssStyle.data}>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.name}>{firstRow}</div>
|
||||
<div className={cssStyle.delete} onClick={() => events.fire("action_entry_delete", { entryId: props.entryId })}>
|
||||
<img src="img/icon_conversation_message_delete.svg" alt="X" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cssStyle.row + " " + cssStyle.second}>
|
||||
<div className={cssStyle.description} title={secondRowTitle}>{secondRow}</div>
|
||||
<div className={cssStyle.length}>{length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const MusicPlaylistList = (props: { events: Registry<MusicPlaylistUiEvents>, className?: string }) => {
|
||||
const [ state, setState ] = useState<MusicPlaylistStatus>(() => {
|
||||
props.events.fire("query_playlist_status");
|
||||
|
||||
return {
|
||||
status: "loading"
|
||||
};
|
||||
});
|
||||
props.events.reactUse("notify_playlist_status", event => setState(event.status));
|
||||
|
||||
let content;
|
||||
switch (state.status) {
|
||||
case "error":
|
||||
content = (
|
||||
<div className={cssStyle.overlay + " " + cssStyle.error} key={"error"}>
|
||||
<a><Translatable>An error occurred while fetching the playlist:</Translatable></a>
|
||||
<a>{state.reason}</a>
|
||||
|
||||
<Button color={"blue"} type={"small"} className={cssStyle.button} onClick={() => props.events.fire("action_load_playlist", { forced: false })}>
|
||||
<Translatable>Reload</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "no-permissions":
|
||||
content = (
|
||||
<div className={cssStyle.overlay} key={"no-permissions"}>
|
||||
<a><Translatable>You don't have permissions to see this playlist:</Translatable></a>
|
||||
<a>
|
||||
<Translatable>Failed on permission</Translatable>
|
||||
<code>{state.failedPermission}</code>
|
||||
</a>
|
||||
|
||||
<Button color={"blue"} type={"small"} className={cssStyle.button} onClick={() => props.events.fire("action_load_playlist", { forced: false })}>
|
||||
<Translatable>Reload</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "unloaded":
|
||||
content = (
|
||||
<div className={cssStyle.overlay} key={"unloaded"}>
|
||||
<a><Translatable>Playlist hasn't been loaded</Translatable></a>
|
||||
<Button color={"blue"} type={"small"} className={cssStyle.button} onClick={() => props.events.fire("action_load_playlist", { forced: false })}>
|
||||
<Translatable>Load playlist</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
content = (
|
||||
<div className={cssStyle.overlay} key={"loading"}>
|
||||
<a><Translatable>Fetching playlist</Translatable> <LoadingDots /></a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "unselected":
|
||||
content = (
|
||||
<div className={cssStyle.overlay} key={"unselected"}>
|
||||
<a><Translatable>Please select a playlist</Translatable></a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "loaded":
|
||||
content = (
|
||||
<div className={cssStyle.playlist} key={"playlist"}>
|
||||
{state.songs.map(songId => <PlaylistEntry entryId={songId} key={"song-" + songId} active={songId === state.activeSong} playlistId={state.playlistId} serverUniqueId={state.serverUniqueId} />)}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={props.events}>
|
||||
<div className={cssStyle.containerPlaylist + " " + props.className}>
|
||||
{content}
|
||||
</div>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
|
@ -1,906 +0,0 @@
|
|||
// import {SideBarController, FrameContent} from "../SideBarController";
|
||||
// import {LogCategory} from "../../../log";
|
||||
// import {CommandResult, PlaylistSong} from "../../../connection/ServerConnectionDeclaration";
|
||||
// import {createErrorModal, createInputModal} from "../../../ui/elements/Modal";
|
||||
// import * as log from "../../../log";
|
||||
// import * as image_preview from "../image_preview";
|
||||
// import {Registry} from "../../../events";
|
||||
// import {ErrorCode} from "../../../connection/ErrorCode";
|
||||
// import {ClientEvents, MusicClientEntry, SongInfo} from "../../../tree/Client";
|
||||
// import { tr } from "tc-shared/i18n/localize";
|
||||
//
|
||||
// export interface MusicSidebarEvents {
|
||||
// "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": ClientEvents["music_status_update"],
|
||||
// "player_song_change": ClientEvents["music_song_change"],
|
||||
//
|
||||
// "playlist_song_add": ClientEvents["playlist_song_add"] & { insert_effect?: boolean },
|
||||
// "playlist_song_remove": ClientEvents["playlist_song_remove"],
|
||||
// "playlist_song_reorder": ClientEvents["playlist_song_reorder"],
|
||||
// "playlist_song_loaded": ClientEvents["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||
// }
|
||||
//
|
||||
// interface LoadedSongData {
|
||||
// description: string;
|
||||
// title: string;
|
||||
// url: string;
|
||||
//
|
||||
// length: number;
|
||||
// thumbnail?: string;
|
||||
//
|
||||
// metadata: {[key: string]: string};
|
||||
// }
|
||||
//
|
||||
// export class MusicInfo {
|
||||
// readonly events: Registry<MusicSidebarEvents>;
|
||||
// readonly handle: SideBarController;
|
||||
//
|
||||
// private _html_tag: JQuery;
|
||||
// private _container_playlist: JQuery;
|
||||
//
|
||||
// private currentMusicBot: 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: SideBarController) {
|
||||
// this.events = new Registry<MusicSidebarEvents>();
|
||||
// this.handle = handle;
|
||||
//
|
||||
// this.events.enableDebug("music-info");
|
||||
// this.initialize_listener();
|
||||
// this._build_html_tag();
|
||||
//
|
||||
// this.set_current_bot(undefined, true);
|
||||
// }
|
||||
//
|
||||
// html_tag() : JQuery {
|
||||
// return this._html_tag;
|
||||
// }
|
||||
//
|
||||
// destroy() {
|
||||
// this.set_current_bot(undefined);
|
||||
// this.events.destroy();
|
||||
//
|
||||
// this._html_tag && this._html_tag.remove();
|
||||
// this._html_tag = undefined;
|
||||
//
|
||||
// this.currentMusicBot = undefined;
|
||||
// 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"));
|
||||
// this._html_tag.find(".thumbnail").on('click', event => {
|
||||
// const image = this._html_tag.find(".thumbnail img");
|
||||
// const url = image.attr("x-thumbnail-url");
|
||||
// if(!url) return;
|
||||
//
|
||||
// image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url));
|
||||
// });
|
||||
//
|
||||
// {
|
||||
// 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.currentMusicBot === undefined || this.currentMusicBot.isCurrentlyPlaying());
|
||||
// });
|
||||
//
|
||||
// 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.currentMusicBot !== undefined && !this.currentMusicBot.isCurrentlyPlaying());
|
||||
// });
|
||||
//
|
||||
// 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.currentMusicBot) {
|
||||
// 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.currentMusicBot.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.currentMusicBot) {
|
||||
// this.update_song_info = 0;
|
||||
// this.currentMusicBot.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")
|
||||
// .attr("x-thumbnail-url", encodeURIComponent(song.song_thumbnail))
|
||||
// .css("cursor", song.song_thumbnail ? "pointer" : null);
|
||||
//
|
||||
// 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.currentMusicBot) return;
|
||||
//
|
||||
// this.currentMusicBot.requestPlayerInfo(0); /* enforce an info refresh */
|
||||
// });
|
||||
//
|
||||
// /* bot property listener */
|
||||
// const callback_property = (event: ClientEvents["notify_properties_updated"]) => this.events.fire("bot_property_update", { properties: Object.keys(event.updated_properties) });
|
||||
// const callback_time_update = (event: ClientEvents["music_status_update"]) => this.events.fire("player_time_update", event, true);
|
||||
// const callback_song_change = (event: ClientEvents["music_song_change"]) => this.events.fire("player_song_change", event, true);
|
||||
// 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.disconnectAll(this.events);
|
||||
// }
|
||||
// if(event.new) {
|
||||
// event.new.events.on("notify_properties_updated", callback_property);
|
||||
//
|
||||
// event.new.events.on("music_status_update", callback_time_update);
|
||||
// event.new.events.on("music_song_change", callback_song_change);
|
||||
//
|
||||
// // @ts-ignore
|
||||
// event.new.events.connect("playlist_song_add", this.events);
|
||||
//
|
||||
// // @ts-ignore
|
||||
// event.new.events.connect("playlist_song_remove", this.events);
|
||||
//
|
||||
// // @ts-ignore
|
||||
// event.new.events.connect("playlist_song_reorder", this.events);
|
||||
//
|
||||
// // @ts-ignore
|
||||
// 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.currentMusicBot) 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 = {
|
||||
// bot_id: this.currentMusicBot.properties.client_database_id,
|
||||
// action: action_id,
|
||||
// units: event.units
|
||||
// };
|
||||
// this.handle.handle.serverConnection.send_command("musicbotplayeraction", data).catch(error => {
|
||||
// if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) 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.currentMusicBot) return;
|
||||
//
|
||||
// const connection = this.handle.handle.serverConnection;
|
||||
// if(!connection || !connection.connected()) return;
|
||||
//
|
||||
// connection.send_command("playlistsongsetcurrent", {
|
||||
// playlist_id: this.currentMusicBot.properties.client_playlist_id,
|
||||
// song_id: event.song_id
|
||||
// }).catch(error => {
|
||||
// if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) 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.currentMusicBot) 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.currentMusicBot) return;
|
||||
//
|
||||
// const connection = this.handle.handle.serverConnection;
|
||||
// connection.send_command("playlistsongadd", {
|
||||
// playlist_id: this.currentMusicBot.properties.client_playlist_id,
|
||||
// url: result
|
||||
// }).catch(error => {
|
||||
// if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) 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.currentMusicBot) return;
|
||||
//
|
||||
// const connection = this.handle.handle.serverConnection;
|
||||
// if(!connection || !connection.connected()) return;
|
||||
//
|
||||
// connection.send_command("playlistsongremove", {
|
||||
// playlist_id: this.currentMusicBot.properties.client_playlist_id,
|
||||
// song_id: event.song_id
|
||||
// }).catch(error => {
|
||||
// if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) 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.currentMusicBot ? this.currentMusicBot.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)
|
||||
// .attr("x-thumbnail-url", encodeURIComponent(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.currentMusicBot) return;
|
||||
//
|
||||
// connection.send_command("playlistsongreorder", {
|
||||
// playlist_id: this.currentMusicBot.properties.client_playlist_id,
|
||||
// song_id: data.song_id,
|
||||
// song_previous_song_id: data.previous_entry
|
||||
// }).catch(error => {
|
||||
// if(error instanceof CommandResult && error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) 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.currentMusicBot) {
|
||||
// this._html_tag.find(".playlist .current-song").removeClass("current-song");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// this.currentMusicBot.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) {
|
||||
// if(client) client.updateClientVariables(); /* just to ensure */
|
||||
// if(client === this.currentMusicBot && (typeof(enforce) === "undefined" || !enforce))
|
||||
// return;
|
||||
//
|
||||
// const old = this.currentMusicBot;
|
||||
// this.currentMusicBot = client;
|
||||
// this.events.fire("bot_change", {
|
||||
// new: client,
|
||||
// old: old
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// current_bot() : MusicClientEntry | undefined {
|
||||
// return this.currentMusicBot;
|
||||
// }
|
||||
//
|
||||
// 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.currentMusicBot) {
|
||||
// this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const overlay_loading = this._container_playlist.find(".overlay-loading");
|
||||
// overlay_loading.removeClass("hidden");
|
||||
//
|
||||
// this.currentMusicBot.updateClientVariables(true).catch(error => {
|
||||
// log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
||||
// }).then(() => {
|
||||
// this.handle.handle.serverConnection.command_helper.requestPlaylistSongs(this.currentMusicBot.properties.client_playlist_id, false).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 === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
// 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.currentMusicBot) {
|
||||
// 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.currentMusicBot.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.find(".container-thumbnail").on('click', event => {
|
||||
// const target = tag.find(".container-thumbnail img");
|
||||
// const url = target.attr("x-thumbnail-url");
|
||||
// if(!url) return;
|
||||
//
|
||||
// image_preview.preview_image(decodeURIComponent(url), decodeURIComponent(url));
|
||||
// });
|
||||
// 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.currentMusicBot) {
|
||||
// this.currentMusicBot.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;
|
||||
// }
|
||||
// }
|
|
@ -308,7 +308,7 @@ export function spawnMusicBotVolumeChange(client: MusicClientEntry, maxValue: nu
|
|||
});
|
||||
});
|
||||
|
||||
const modal = spawnReactModal(VolumeChangeBot, client, events, maxValue);
|
||||
const modal = spawnReactModal(VolumeChangeBot, client as any, events, maxValue);
|
||||
|
||||
events.on("close-modal", event => modal.destroy());
|
||||
modal.show();
|
||||
|
|
|
@ -62,10 +62,11 @@ export class Slider extends React.Component<SliderProperties, SliderState> {
|
|||
|
||||
const range = this.props.maxValue - this.props.minValue;
|
||||
let offset = Math.round(((current - min) * (range / this.props.stepSize)) / (max - min)) * this.props.stepSize;
|
||||
if(offset < 0)
|
||||
if(offset < 0) {
|
||||
offset = 0;
|
||||
else if(offset > range)
|
||||
} else if(offset > range) {
|
||||
offset = range;
|
||||
}
|
||||
|
||||
this.refTooltip.current?.setState({
|
||||
pageX: bounds.left + offset * bounds.width / range,
|
||||
|
@ -120,7 +121,14 @@ export class Slider extends React.Component<SliderProperties, SliderState> {
|
|||
|
||||
render() {
|
||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : this.props.disabled;
|
||||
const offset = (this.state.value - this.props.minValue) * 100 / (this.props.maxValue - this.props.minValue);
|
||||
|
||||
let value = this.state.value;
|
||||
if(value > this.props.maxValue) {
|
||||
value = this.props.maxValue;
|
||||
} else if(value < this.props.minValue) {
|
||||
value = this.props.minValue;
|
||||
}
|
||||
const offset = (value - this.props.minValue) * 100 / (this.props.maxValue - this.props.minValue);
|
||||
return (
|
||||
<div
|
||||
className={cssStyle.container + " " + (this.props.className || " ") + " " + (disabled ? cssStyle.disabled : "")}
|
||||
|
|
Loading…
Reference in New Issue