Implemented a music bot control panel

canary
WolverinDEV 2020-02-22 14:30:17 +01:00
parent 7fbc84128e
commit 78410c9fdf
22 changed files with 3899 additions and 259 deletions

View File

@ -1,4 +1,7 @@
# Changelog:
* **22.02.20**
- Added a music bot control panel
* **16.02.20**
- Updated the `setup_windows.md` tutorial
- Correct redirecting to `index.php` when using the serve mode

View File

@ -31,7 +31,7 @@ type ProjectResource = {
const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
{ /* shared html and php files */
"type": "html",
"search-pattern": /^([a-zA-Z]+)\.(html|php|json)$/,
"search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
"build-target": "dev|rel",
"path": "./",
@ -1151,7 +1151,7 @@ async function main(args: string[]) {
}
/* proxy log for better format */
const wrap_log = (original, prefix: string) => (message, ...args) => original(prefix + message.replace(/\n/g, "\n" + prefix), ...args.map(e => typeof(e) === "string" ? e.replace(/\n/g, "\n" + prefix) : e));
const wrap_log = (original, prefix: string) => (message, ...args) => original(prefix + (message ? message + "" : "").replace(/\n/g, "\n" + prefix), ...args.map(e => typeof(e) === "string" ? e.replace(/\n/g, "\n" + prefix) : e));
console.log = wrap_log(console.log, "[INFO ] ");
console.debug = wrap_log(console.debug, "[DEBUG] ");
console.warn = wrap_log(console.warn, "[WARNING] ");

View File

@ -149,3 +149,32 @@
overflow: hidden;
text-overflow: ellipsis;
}
@mixin tooltip($base_with, $icon_size) {
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: $base_with;
margin-left: .5em;
margin-right: .5em;
font-size: .9em;
display: flex;
flex-direction: column;
justify-content: center;
img, .icon_em {
height: $icon_size;
width: $icon_size;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
}

View File

@ -0,0 +1,697 @@
@import "mixin";
@import "properties";
.modal-body.modal-music-manage {
padding: 0 !important;
display: flex !important;;
flex-direction: column !important;;
justify-content: stretch !important;;
width: 80em;
min-height: 20em; /* Set it here, so we dont have a inner modal scroll */
@include user-select(none);
> .header {
flex-grow: 0;
flex-shrink: 0;
height: 4em;
display: flex;
flex-direction: row;
justify-content: stretch;
.category {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
width: 50%;
position: relative;
overflow: hidden;
cursor: pointer;
padding-bottom: 2px;
display: flex;
flex-direction: column;
justify-content: center;
a {
text-align: center;
color: #e1e1e1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
border: none;
border-bottom: 2px solid #4e4e4e;
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px #424242;
}
}
&.selected {
border: none;
border-bottom: 2px solid #0073d4;
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px #0073d4;
}
}
}
}
> .body {
display: flex;
flex-direction: column;
justify-content: stretch;
min-height: 20em;
background-color: #303036;
@include tooltip(1.6em, 1em);
.container {
flex-grow: 0;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
min-height: 20em;
height: 40.5em;
.input-boxed, .btn {
height: 2em;
}
$border_color: #1e2025;
&.category-permissions {
.column {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
&.column-permission {
flex-shrink: 1000;
flex-grow: 1;
min-width: 6em;
a {
@include text-dotdotdot();
}
.master {
font-weight: bold;
}
.slave {
padding-left: 1em;
}
}
&.column-required {
flex-shrink: 1;
flex-grow: 0;
a {
@include text-dotdotdot();
}
min-width: 6em;
width: 10em;
}
&.column-client-specific {
flex-shrink: 1;
flex-grow: 0;
min-width: 20em;
width: 30em;
}
}
.table-head {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
.column {
height: 5.5em;
padding: .5em;
justify-content: flex-end!important;
&.column-permission, &.column-required {
color: #e1e1e1;
font-weight: bold;
border-right: 1px solid $border_color;
}
}
.select-client {
padding-top: .5em;
display: flex;
flex-direction: row;
justify-content: stretch;
input {
flex-grow: 1;
flex-shrink: 1;
min-width: 1em;
}
button {
flex-grow: 0;
flex-shrink: 0;
width: 5em;
margin-left: 1em;
}
}
.client-select, .client-info {
display: flex;
flex-direction: column;
justify-content: stretch;
&.hidden {
display: none;
}
button {
flex-grow: 0;
flex-shrink: 0;
margin-left: 1em;
@include text-dotdotdot();
&.button-search {
width: 6em;
}
&.button-list-clients, &.button-client-deselect {
width: 8em;
}
}
.row {
height: 2em;
display: flex;
flex-direction: row;
justify-content: stretch;
&:not(:first-of-type) {
margin-top: .5em;
}
a {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
align-self: center;
@include text-dotdotdot();
}
input {
flex-grow: 1;
flex-shrink: 1;
min-width: 2em;
}
}
}
.client-info {
color: #e1e1e1;
font-weight: bold;
a {
flex-shrink: 0;
margin-right: .5em;
}
.htmltag-client {
color: #e1e1e1;
align-self: center;
@include text-dotdotdot();
}
}
}
.table-body {
flex-shrink: 1;
flex-grow: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
min-height: 6em;
overflow-x: hidden;
overflow-y: auto;
@include chat-scrollbar-vertical();
.entry {
height: 2.6em; /* input box + 2 * .5em */
padding: .5em;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: center;
background-color: unset;
&:nth-of-type(2n) {
background-color: #25252a;
}
border-top: 1px solid $border_color;
border-right: 1px solid $border_color;
.container-input {
color: #e1e1e1;
display: flex;
flex-direction: row;
justify-content: stretch;
&.hidden {
display: none;
}
input {
text-align: right;
flex-shrink: 1;
flex-grow: 1;
min-width: 2em;
outline: none;
background: transparent;
border: none;
height: 1.6em;
/* fix the column padding */
padding-left: 1em;
margin-left: -.5em; /* have a bit of space on both sides */
color: #999;
}
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
}
border-bottom: 2px solid transparent;
&:focus-within {
border-bottom-color: #3f7dbf;
input {
color: #e1e1e1;
}
}
@include transition(border-bottom-color $button_hover_animation_time ease-in-out);
}
}
.column-client-specific {
position: relative;
.entry {
border-right: unset;
}
.overlay-client-list {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: stretch;
border-top: 1px solid $border_color;
background-color: #303036;
padding: .5em;
opacity: 1;
pointer-events: all;
@include transition(all .25s ease-in-out);
&.hidden {
pointer-events: none;
opacity: 0;
}
.title {
flex-grow: 0;
flex-shrink: 0;
}
.container-client-list {
flex-grow: 1;
flex-shrink: 1;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-start;
border: 1px #161616 solid;
border-radius: 0.2em;
background-color: #28292b;
overflow-x: hidden;
overflow-y: auto;
padding-top: 0.25em;
padding-bottom: 0.25em;
margin-top: .5em;
margin-bottom: .5em;
@include chat-scrollbar-vertical();
.overlay {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
background-color: #28292b;
color: #676468;
text-align: center;
font-size: 1.3em;
&.hidden {
display: none;
}
}
.client {
padding-left: .5em;
padding-right: .5em;
.htmltag-client {
@include text-dotdotdot();
color: #999;
font-weight: unset;
}
&:hover {
background-color: #2c2d2f;
}
&.hidden {
display: none;
}
}
}
.container-buttons {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
}
}
}
.container-buttons {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: .5em;
}
}
&.category-settings {
.container-settings {
flex-grow: 1;
flex-shrink: 1;
min-height: 10em;
display: flex;
flex-direction: row;
justify-content: stretch;
.settings {
flex-grow: 1;
flex-shrink: 1;
width: 50%;
min-width: 15em;
display: flex;
flex-direction: column;
justify-content: stretch;
padding: .5em;
&.settings-bot {
border-right: 1px solid $border_color;
padding-left: 0;
}
&.settings-playlist {
padding-right: 0;
}
> a {
color: #e1e1e1;
font-weight: bold;
}
.body {
display: flex;
flex-direction: column;
justify-content: flex-start;
> label, > div {
display: flex;
justify-content: flex-start;
flex-direction: row;
margin-top: .5em;
* {
align-self: center;
}
}
a {
@include text-dotdotdot();
}
.container-name-country {
display: flex;
flex-direction: row;
justify-content: stretch;
.option-bot-name {
flex-grow: 1;
flex-shrink: 1;
min-width: 6em;
margin-right: 1em;
}
.container-country {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
width: 5em;
input {
flex-shrink: 1;
flex-grow: 1;
min-width: 0;
}
.country {
flex-grow: 0;
flex-shrink: 0;
margin: .5em;
}
}
}
.checkbox {
margin-right: .5em;
}
.container-replay-mode, .container-max-playlist-size {
display: flex;
flex-direction: row;
justify-content: stretch;
a {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
select, .input-boxed {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
width: 10em;
margin-left: .5em;
input {
flex-shrink: 1;
flex-grow: 1;
min-width: 0;
}
}
}
}
}
}
.container-buttons {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
}
&.hidden {
display: none;
}
}
}
}
.tooltip-music-permission-overview {
padding-left: .25em;
padding-right: .25em;
text-align: left;
display: flex;
flex-direction: column;
justify-content: flex-start;
max-height: 8em;
.container-title {
flex-grow: 0;
flex-shrink: 0;
}
.container-groups {
flex-grow: 0;
flex-shrink: 1;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow-x: hidden;
overflow-y: auto;
@include chat-scrollbar-horizontal();
}
.container-status {
&.hidden {
display: none;
}
}
}

View File

@ -0,0 +1,574 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Template Music manage</title>
</head>
<body>
<script class="jsrender-template" id="tmpl_music_manage" type="text/html">
<div>
<div class="header">
<div class="category category-settings">
<a>{{tr "Settings" /}}</a>
</div>
<div class="category category-permissions">
<a>{{tr "Permissions" /}}</a>
</div>
</div>
<div class="body">
<div class="container category-settings">
<div class="container-settings">
<div class="settings settings-bot">
<a>{{tr "Music Bot" /}}</a>
<div class="body">
<div class="container-name-country">
<input class="input-boxed option-bot-name" placeholder="Bot name" max="30">
<div class="input-boxed container-country">
<input class="option-bot-country" maxlength="2">
<div class="country flag-de"></div>
</div>
</div>
<label>
<div class="checkbox">
<input type="checkbox" class="option-channel-commander">
<div class="mark"></div>
</div>
<a>{{tr "Music bot is channel commander" /}}</a>
</label>
<label>
<div class="checkbox">
<input type="checkbox" class="option-priority-speaker">
<div class="mark"></div>
</div>
<a>{{tr "Music bot is priority speaker" /}}</a>
</label>
</div>
</div>
<div class="settings settings-playlist">
<a>{{tr "Replay" /}}</a>
<div class="body">
<div class="container-replay-mode">
<a>{{tr "Replay mode" /}}</a>
<select class="input-boxed option-replay-mode">
<option value="-1" style="display: none;">{{tr "Unknown" /}}</option>
<option value="0">{{tr "Normal" /}}</option>
<option value="1">{{tr "Looped" /}}</option>
<option value="2">{{tr "Single-Looped" /}}</option>
<option value="3">{{tr "Shuffle" /}}</option>
</select>
</div>
<div class="container-max-playlist-size">
<a>{{tr "Max playlist size" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="60" maxlength="6">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a>You could set this value up to X.</a>
</div>
</div>
</div>
</div>
<label>
<div class="checkbox">
<input type="checkbox" class="option-delete-played-songs">
<div class="mark"></div>
</div>
<a>{{tr "Delete played songs from playlist" /}}</a>
</label>
<label>
<div class="checkbox">
<input type="checkbox" class="option-notify-songs-change">
<div class="mark"></div>
</div>
<a>{{tr "Send a channel message when the song changes" /}}</a>
</label>
</div>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-blue button-reload">{{tr "Reload" /}}</button>
</div>
</div>
<div class="container category-permissions">
<div class="table-head">
<div class="column column-permission">
<a>{{tr "Permission" /}}</a>
</div>
<div class="column column-required">
<a>{{tr "Required value" /}}</a>
</div>
<div class="column column-client-specific">
<div class="client-select">
<div class="row">
<a>{{tr "Search for an client:" /}}</a>
<button class="btn btn-blue button-list-clients">error: client list</button>
</div>
<div class="row">
<input type="text" class="input-boxed input-search" placeholder="error: placeholder" maxlength="40">
<button class="btn btn-green button-search">{{tr "Search" /}}</button>
</div>
</div>
<div class="client-info hidden">
<div class="row">
<a></a>
<button class="btn btn-blue button-client-deselect">{{tr "Unselect" /}}</button>
</div>
<div class="row container-selected-client">
<a>{{tr "Showing permissions for:" /}}</a>
</div>
</div>
</div>
</div>
<div class="table-body">
<div class="column column-permission">
<div class="entry master">
<a>{{tr "Bot permissions" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to rename the music bot" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to modify the music bot permissions" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to delete the music bot" /}}</a>
</div>
<div class="entry master">
<a>{{tr "Playlist permissions" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to see the playlist songs" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power for editing playlist settings" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power for viewing playlist permissions" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power for editing playlist permissions" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power for adding a song to the playlist" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to reorder a song within the playlist" /}}</a>
</div>
<div class="entry slave">
<a>{{tr "Power to delete a song from the playlist" /}}</a>
</div>
</div>
<div class="column column-required">
<div class="entry"></div> <!-- bot permissions -->
<div class="entry">
<div class="container-input general-permission" x-permission="i_client_music_needed_rename_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could rename the bot:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_client_music_needed_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could modify the bot:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}<br>
b_virtualserver_permission_find
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_client_music_needed_delete_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could delete the bot:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry"></div> <!-- play permissions -->
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_needed_view_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could view the playlist:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_needed_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could edit the playlist settings:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="b_virtualserver_playlist_permission_list">
<div style="flex-grow: 1; flex-shrink: 1; min-width: 0"></div>
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could view the playlist permissions:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_needed_permission_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could edit the playlist permissions:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_song_needed_add_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could add a song:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_song_needed_move_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could reorder the songs:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input general-permission" x-permission="i_playlist_song_needed_remove_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<img src="img/icon_tooltip_music_permission.svg">
<div class="tooltip">
<a class="tooltip-music-permission-overview">
<div class="container-title">{{tr "These groups could delete a song:" /}}</div>
<div class="container-status status-no-groups">
{{tr "No group could do that." /}}
</div>
<div class="container-status status-loading">
{{tr "loading..." /}}
</div>
<div class="container-status status-error-permission">
{{tr "failed on permission" /}}
</div>
<div class="container-status status-error">
error: error
</div>
<div class="container-groups"> </div>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="column column-client-specific">
<div class="entry"></div> <!-- bot permissions -->
<div class="entry">
<div class="container-input client-permission" x-permission="i_client_music_rename_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-apply"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_client_music_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_client_music_delete_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry"></div> <!-- play permissions -->
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_view_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="b_virtualserver_playlist_permission_list">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could no see the permissions" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_permission_modify_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_song_add_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_song_move_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="entry">
<div class="container-input client-permission" x-permission="i_playlist_song_remove_power">
<input type="number" min="0" value="60">
<div class="container-tooltip">
<div class="icon_em client-delete"></div>
<div class="tooltip">
<a>{{tr "Client could perform this action" /}}</a>
</div>
</div>
</div>
</div>
<div class="overlay-client-list">
<div class="title">{{tr "Clients which have special permissions:" /}}</div>
<div class="container-client-list">
<div class="overlay overlay-filter-no-result">
<a>{{tr "No clients match the filter" /}}</a>
</div>
<div class="overlay overlay-empty-list">
<a>{{tr "No clients available" /}}</a>
</div>
<div class="overlay overlay-query-error">
<a>error: error text</a>
</div>
<div class="overlay overlay-query-error-permissions">
<a>error: permission error text</a>
</div>
<div class="overlay overlay-refreshing">
<a>{{tr "Loading..." /}}</a>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-blue button-clientlist-refresh">{{tr "Refresh client list" /}}</button>
</div>
</div>
</div>
</div>
<div class="container-buttons">
<button class="btn btn-blue button-permission-refresh">{{tr "Refresh" /}}</button>
</div>
</div>
</div>
</div>
</script>
</body>
</html>

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
<defs>
<style>
.cls-1 {
fill: #999;
fill-rule: evenodd;
}
</style>
</defs>
<path id="icon" class="cls-1" d="M200.012,0c110.457,0,200,89.545,200,200s-89.54,200-200,200S0.015,310.456.015,200,89.557,0,200.012,0Zm0.01,38.636A160.629,160.629,0,1,1,39.395,199.268,160.632,160.632,0,0,1,200.022,38.639ZM159.407,153.478s42.867-5.468,19.8,68.014c-24.208,77.113-66.566,140.45,63.467,81.447,0,0-64.556,21.6-42.619-37.407,12.79-34.406,28.292-81.421,27.092-93.4C225.387,154.546,207.6,141.025,159.407,153.478Zm65.564-79a24.685,24.685,0,1,1-24.687,24.684A24.685,24.685,0,0,1,224.971,74.479Z"/>
</svg>

After

Width:  |  Height:  |  Size: 757 B

View File

@ -704,9 +704,12 @@ class ConnectionHandler {
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
const active = !this.client_status.input_muted && !this.client_status.output_muted;
/* No need to start the microphone when we're not even connected */
const input = vconnection.voice_recorder().input;
if(input) {
if(active && this.serverConnection.connected()) {
if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
vconnection.voice_recorder().input.start().then(result => {
if(input.current_state() === audio.recorder.InputState.PAUSED) {
input.start().then(result => {
if(result != audio.recorder.InputStartResult.EOK)
throw result;
}).catch(error => {
@ -718,7 +721,8 @@ class ConnectionHandler {
});
}
} else {
vconnection.voice_recorder().input.stop();
input.stop();
}
}
}

View File

@ -2,6 +2,7 @@ namespace connection {
export class CommandHelper extends AbstractCommandHandler {
private _who_am_i: any;
private _awaiters_unique_ids: {[unique_id: string]:((resolved: ClientNameInfo) => any)[]} = {};
private _awaiters_unique_dbid: {[database_id: number]:((resolved: ClientNameInfo) => any)[]} = {};
constructor(connection) {
super(connection);
@ -25,6 +26,8 @@ namespace connection {
handle_command(command: connection.ServerCommand): boolean {
if(command.command == "notifyclientnamefromuid")
this.handle_notifyclientnamefromuid(command.arguments);
if(command.command == "notifyclientgetnamefromdbid")
this.handle_notifyclientgetnamefromdbid(command.arguments);
else
return false;
return true;
@ -57,6 +60,8 @@ namespace connection {
const response: ClientNameInfo[] = [];
const request = [];
const unique_ids = new Set(_unique_ids);
if(!unique_ids.size) return [];
const unique_id_resolvers: {[unique_id: string]: (resolved: ClientNameInfo) => any} = {};
@ -83,6 +88,54 @@ namespace connection {
return response;
}
private handle_notifyclientgetnamefromdbid(json: any[]) {
for(const entry of json) {
const info: ClientNameInfo = {
client_unique_id: entry["cluid"],
client_nickname: entry["clname"],
client_database_id: parseInt(entry["cldbid"])
};
const functions = this._awaiters_unique_dbid[info.client_database_id] || [];
delete this._awaiters_unique_dbid[info.client_database_id];
for(const fn of functions)
fn(info);
}
}
async info_from_cldbid(..._cldbid: number[]) : Promise<ClientNameInfo[]> {
const response: ClientNameInfo[] = [];
const request = [];
const unique_cldbid = new Set(_cldbid);
if(!unique_cldbid.size) return [];
const unique_cldbid_resolvers: {[dbid: number]: (resolved: ClientNameInfo) => any} = {};
for(const cldbid of unique_cldbid) {
request.push({'cldbid': cldbid});
(this._awaiters_unique_dbid[cldbid] || (this._awaiters_unique_dbid[cldbid] = []))
.push(unique_cldbid_resolvers[cldbid] = info => response.push(info));
}
try {
await this.connection.send_command("clientgetnamefromdbid", request);
} catch(error) {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
/* nothing */
} else {
throw error;
}
} finally {
/* cleanup */
for(const cldbid of Object.keys(unique_cldbid_resolvers))
(this._awaiters_unique_dbid[cldbid] || []).remove(unique_cldbid_resolvers[cldbid]);
}
return response;
}
private handle_notifyclientnamefromuid(json: any[]) {
for(const entry of json) {
const info: ClientNameInfo = {
@ -245,6 +298,40 @@ namespace connection {
});
}
request_playlist_client_list(playlist_id: number) : Promise<number[]> {
return new Promise((resolve, reject) => {
const single_handler: SingleCommandHandler = {
command: "notifyplaylistclientlist",
function: command => {
const json = command.arguments;
if(json[0]["playlist_id"] != playlist_id) {
log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist clients"));
return false;
}
const result: number[] = [];
for(const entry of json)
result.push(parseInt(entry["cldbid"]));
resolve(result.filter(e => !isNaN(e)));
return true;
}
};
this.handler_boss.register_single_handler(single_handler);
this.connection.send_command("playlistclientlist", {playlist_id: playlist_id}).catch(error => {
this.handler_boss.remove_single_handler(single_handler);
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
resolve([]);
return;
}
reject(error);
})
});
}
async request_clients_by_server_group(group_id: number) : Promise<ServerGroupClient[]> {
//servergroupclientlist sgid=2
//notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw=
@ -309,6 +396,8 @@ namespace connection {
playlist_flag_finished: json["playlist_flag_finished"] == true || json["playlist_flag_finished"] == "1",
playlist_replay_mode: parseInt(json["playlist_replay_mode"]),
playlist_current_song_id: parseInt(json["playlist_current_song_id"]),
playlist_max_songs: parseInt(json["playlist_max_songs"])
});
} catch (error) {
log.error(LogCategory.NETWORKING, tr("Failed to parse playlist info: %o"), error);

View File

@ -96,6 +96,8 @@ interface PlaylistInfo {
playlist_flag_finished: boolean,
playlist_replay_mode: number,
playlist_current_song_id: number,
playlist_max_songs: number
}
interface PlaylistSong {

View File

@ -160,6 +160,160 @@ namespace events {
}
}
export namespace modal {
export type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker";
export type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change";
export interface music_manage {
show_container: { container: "settings" | "permissions"; };
/* setting relevant */
query_bot_status: {},
bot_status: {
status: "success" | "error";
error_msg?: string;
data?: {
name: string,
description: string,
volume: number,
country_code: string,
default_country_code: string,
channel_commander: boolean,
priority_speaker: boolean,
client_version: string,
client_platform: string,
uptime_mode: number,
bot_type: number
}
},
set_bot_status: {
key: BotStatusType,
value: any
},
set_bot_status_result: {
key: BotStatusType,
status: "success" | "error" | "timeout",
error_msg?: string,
value?: any
}
query_playlist_status: {},
playlist_status: {
status: "success" | "error",
error_msg?: string,
data?: {
replay_mode: number,
finished: boolean,
delete_played: boolean,
max_size: number,
notify_song_change: boolean
}
},
set_playlist_status: {
key: PlaylistStatusType,
value: any
},
set_playlist_status_result: {
key: PlaylistStatusType,
status: "success" | "error" | "timeout",
error_msg?: string,
value?: any
}
/* permission relevant */
show_client_list: {},
hide_client_list: {},
filter_client_list: { filter: string | undefined },
"refresh_permissions": {},
query_special_clients: {},
special_client_list: {
status: "success" | "error" | "error-permission",
error_msg?: string,
clients?: {
name: string,
unique_id: string,
database_id: number
}[]
},
search_client: { text: string },
search_client_result: {
status: "error" | "timeout" | "empty" | "success",
error_msg?: string,
client?: {
name: string,
unique_id: string,
database_id: number
}
},
/* sets a client to set the permission for */
special_client_set: {
client?: {
name: string,
unique_id: string,
database_id: number
}
},
"query_general_permissions": {},
"general_permissions": {
status: "error" | "timeout" | "success",
error_msg?: string,
permissions?: {[key: string]:number}
},
"set_general_permission_result": {
status: "error" | "success",
key: string,
value?: number,
error_msg?: string
},
"set_general_permission": { /* try to change a permission for the server */
key: string,
value: number
},
"query_client_permissions": { client_database_id: number },
"client_permissions": {
status: "error" | "timeout" | "success",
client_database_id: number,
error_msg?: string,
permissions?: {[key: string]:number}
},
"set_client_permission_result": {
status: "error" | "success",
client_database_id: number,
key: string,
value?: number,
error_msg?: string
},
"set_client_permission": { /* try to change a permission for the server */
client_database_id: number,
key: string,
value: number
},
"query_group_permissions": { permission_name: string },
"group_permissions": {
permission_name: string;
status: "error" | "timeout" | "success"
groups?: {
name: string,
value: number,
id: number
}[],
error_msg?: string
}
}
}
}
const eclient = new events.Registry<events.channel_tree.client>();

View File

@ -1492,7 +1492,7 @@ namespace i18n {
alpha_3: "ZWE",
un_code: 716
});
}
};
export function country_name(alpha_code: string, fallback?: string) {
return (alpha_2_map[alpha_code.toUpperCase()] || {name: fallback || tr("unknown country")}).name;

View File

@ -416,20 +416,68 @@ class NeededPermissionValue extends PermissionValue {
}
}
class ChannelPermissionRequest {
requested: number;
channel_id: number;
callback_success: ((_: PermissionValue[]) => any)[] = [];
callback_error: ((_: any) => any)[] = [];
}
class TeaPermissionRequest {
namespace permissions {
export type PermissionRequestKeys = {
client_id?: number;
channel_id?: number;
playlist_id?: number;
}
export type PermissionRequest = PermissionRequestKeys & {
timeout_id: any;
promise: LaterPromise<PermissionValue[]>;
};
export namespace find {
export type Entry = {
type: "server" | "channel" | "client" | "client_channel" | "channel_group" | "server_group";
value: number;
id: number;
}
export type Client = Entry & {
type: "client",
client_id: number;
}
export type Channel = Entry & {
type: "channel",
channel_id: number;
}
export type Server = Entry & {
type: "server"
}
export type ClientChannel = Entry & {
type: "client_channel",
client_id: number;
channel_id: number;
}
export type ChannelGroup = Entry & {
type: "channel_group",
group_id: number;
}
export type ServerGroup = Entry & {
type: "server_group",
group_id: number;
}
}
}
type RequestLists =
"requests_channel_permissions" |
"requests_client_permissions" |
"requests_client_channel_permissions" |
"requests_playlist_permissions" |
"requests_playlist_client_permissions";
class PermissionManager extends connection.AbstractCommandHandler {
readonly handle: ConnectionHandler;
@ -439,44 +487,50 @@ class PermissionManager extends connection.AbstractCommandHandler {
needed_permission_change_listener: {[permission: string]:(() => any)[]} = {};
requests_channel_permissions: ChannelPermissionRequest[] = [];
requests_client_permissions: TeaPermissionRequest[] = [];
requests_client_channel_permissions: TeaPermissionRequest[] = [];
requests_playlist_permissions: TeaPermissionRequest[] = [];
requests_channel_permissions: permissions.PermissionRequest[] = [];
requests_client_permissions: permissions.PermissionRequest[] = [];
requests_client_channel_permissions: permissions.PermissionRequest[] = [];
requests_playlist_permissions: permissions.PermissionRequest[] = [];
requests_playlist_client_permissions: permissions.PermissionRequest[] = [];
requests_permfind: {
timeout_id: number,
permission: string,
callback: (status: "success" | "error", data: any) => void
}[] = [];
initializedListener: ((initialized: boolean) => void)[] = [];
private _cacheNeededPermissions: any;
/* Static info mapping until TeaSpeak implements a detailed info */
//TODO tr
static readonly group_mapping: {name: string, deep: number}[] = [
{name: "Global", deep: 0},
{name: "Information", deep: 1},
{name: "Virtual server management", deep: 1},
{name: "Administration", deep: 1},
{name: "Settings", deep: 1},
{name: "Virtual Server", deep: 0},
{name: "Information", deep: 1},
{name: "Administration", deep: 1},
{name: "Settings", deep: 1},
{name: "Channel", deep: 0},
{name: "Information", deep: 1},
{name: "Create", deep: 1},
{name: "Modify", deep: 1},
{name: "Delete", deep: 1},
{name: "Access", deep: 1},
{name: "Group", deep: 0},
{name: "Information", deep: 1},
{name: "Create", deep: 1},
{name: "Modify", deep: 1},
{name: "Delete", deep: 1},
{name: "Client", deep: 0},
{name: "Information", deep: 1},
{name: "Admin", deep: 1},
{name: "Basics", deep: 1},
{name: "Modify", deep: 1},
{name: tr("Global"), deep: 0},
{name: tr("Information"), deep: 1},
{name: tr("Virtual server management"), deep: 1},
{name: tr("Administration"), deep: 1},
{name: tr("Settings"), deep: 1},
{name: tr("Virtual Server"), deep: 0},
{name: tr("Information"), deep: 1},
{name: tr("Administration"), deep: 1},
{name: tr("Settings"), deep: 1},
{name: tr("Channel"), deep: 0},
{name: tr("Information"), deep: 1},
{name: tr("Create"), deep: 1},
{name: tr("Modify"), deep: 1},
{name: tr("Delete"), deep: 1},
{name: tr("Access"), deep: 1},
{name: tr("Group"), deep: 0},
{name: tr("Information"), deep: 1},
{name: tr("Create"), deep: 1},
{name: tr("Modify"), deep: 1},
{name: tr("Delete"), deep: 1},
{name: tr("Client"), deep: 0},
{name: tr("Information"), deep: 1},
{name: tr("Admin"), deep: 1},
{name: tr("Basics"), deep: 1},
{name: tr("Modify"), deep: 1},
//TODO Music bot
{name: "File Transfer", deep: 0},
{name: tr("File Transfer"), deep: 0},
];
private _group_mapping;
@ -539,10 +593,10 @@ class PermissionManager extends connection.AbstractCommandHandler {
this.neededPermissions = undefined;
this.requests_channel_permissions = undefined;
this.requests_client_permissions = undefined;
this.requests_client_channel_permissions = undefined;
this.requests_playlist_permissions = undefined;
/* delete all requests */
for(const key of Object.keys(this))
if(key.startsWith("requests"))
delete this[key];
this.initializedListener = undefined;
this._cacheNeededPermissions = undefined;
@ -568,6 +622,9 @@ class PermissionManager extends connection.AbstractCommandHandler {
case "notifyplaylistpermlist":
this.onPlaylistPermList(command.arguments);
return true;
case "notifyplaylistclientpermlist":
this.onPlaylistClientPermList(command.arguments);
return true;
}
return false;
}
@ -708,172 +765,252 @@ class PermissionManager extends connection.AbstractCommandHandler {
return undefined;
}
requestChannelPermissions(channelId: number) : Promise<PermissionValue[]> {
return new Promise<PermissionValue[]>((resolve, reject) => {
let request: ChannelPermissionRequest;
for(let element of this.requests_channel_permissions)
if(element.requested + 1000 < Date.now() && element.channel_id == channelId) {
request = element;
break;
}
if(!request) {
request = new ChannelPermissionRequest();
request.requested = Date.now();
request.channel_id = channelId;
this.handle.serverConnection.send_command("channelpermlist", {"cid": channelId}).catch(error => {
this.requests_channel_permissions.remove(request);
if(error instanceof CommandResult) {
if(error.id == ErrorID.EMPTY_RESULT) {
request.callback_success.forEach(e => e([]));
return;
}
}
request.callback_error.forEach(e => e(error));
}).then(() => {
//Error handler if we've not received an notify
setTimeout(() => {
if(this.requests_channel_permissions.remove(request)) {
request.callback_error.forEach(e => e(tr("missing notify")));
}
}, 1000);
});
this.requests_channel_permissions.push(request);
}
request.callback_error.push(reject);
request.callback_success.push(resolve);
});
}
/* channel permission request */
private onChannelPermList(json) {
let channelId: number = parseInt(json[0]["cid"]);
let permissions = PermissionManager.parse_permission_bulk(json, this.handle.permissions);
log.debug(LogCategory.PERMISSIONS, tr("Got channel permissions for channel %o"), channelId);
for(let element of this.requests_channel_permissions) {
if(element.channel_id == channelId) {
for(let l of element.callback_success)
l(permissions);
this.requests_channel_permissions.remove(element);
return;
this.fullfill_permission_request("requests_channel_permissions", {
channel_id: channelId
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_channel_permission_request(request: permissions.PermissionRequestKeys) {
this.handle.serverConnection.send_command("channelpermlist", {"cid": request.channel_id}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_channel_permissions", request, "success", []);
else
this.fullfill_permission_request("requests_channel_permissions", request, "error", error);
});
}
log.debug(LogCategory.PERMISSIONS, tr("Missing channel permission handle for requested channel id %o"), channelId);
requestChannelPermissions(channelId: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
channel_id: channelId
};
return this.execute_permission_request("requests_channel_permissions", keys, this.execute_channel_permission_request.bind(this));
}
/* client permission request */
private onClientPermList(json: any[]) {
let client = parseInt(json[0]["cldbid"]);
this.fullfill_permission_request("requests_client_permissions", {
client_id: client
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_client_permission_request(request: permissions.PermissionRequestKeys) {
this.handle.serverConnection.send_command("clientpermlist", {cldbid: request.client_id}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_client_permissions", request, "success", []);
else
this.fullfill_permission_request("requests_client_permissions", request, "error", error);
});
}
requestClientPermissions(client_id: number) : Promise<PermissionValue[]> {
for(let request of this.requests_client_permissions)
if(request.client_id == client_id && request.promise.time() + 1000 > Date.now())
return request.promise;
let request: TeaPermissionRequest = {} as any;
request.client_id = client_id;
request.promise = new LaterPromise<PermissionValue[]>();
this.handle.serverConnection.send_command("clientpermlist", {cldbid: client_id}).catch(error => {
this.requests_client_permissions.remove(request);
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
request.promise.resolved([]);
else
request.promise.rejected(error);
}).then(() => {
//Error handler if we've not received an notify
setTimeout(() => {
if(this.requests_client_permissions.remove(request)) {
request.promise.rejected(tr("missing notify"));
}
}, 1000);
});
this.requests_client_permissions.push(request);
return request.promise;
}
private onClientPermList(json: any[]) {
let client = parseInt(json[0]["cldbid"]);
let permissions = PermissionManager.parse_permission_bulk(json, this);
for(let req of this.requests_client_permissions.slice(0)) {
if(req.client_id == client) {
this.requests_client_permissions.remove(req);
req.promise.resolved(permissions);
}
}
}
requestClientChannelPermissions(client_id: number, channel_id: number) : Promise<PermissionValue[]> {
for(let request of this.requests_client_channel_permissions)
if(request.client_id == client_id && request.channel_id == channel_id && request.promise.time() + 1000 > Date.now())
return request.promise;
let request: TeaPermissionRequest = {} as any;
request.client_id = client_id;
request.channel_id = channel_id;
request.promise = new LaterPromise<PermissionValue[]>();
this.handle.serverConnection.send_command("channelclientpermlist", {cldbid: client_id, cid: channel_id}).catch(error => {
this.requests_client_channel_permissions.remove(request);
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
request.promise.resolved([]);
else
request.promise.rejected(error);
}).then(() => {
//Error handler if we've not received an notify
setTimeout(() => {
if(this.requests_client_channel_permissions.remove(request)) {
request.promise.rejected(tr("missing notify"));
}
}, 1000);
});
this.requests_client_channel_permissions.push(request);
return request.promise;
const keys: permissions.PermissionRequestKeys = {
client_id: client_id
};
return this.execute_permission_request("requests_client_permissions", keys, this.execute_client_permission_request.bind(this));
}
/* client channel permission request */
private onChannelClientPermList(json: any[]) {
let client_id = parseInt(json[0]["cldbid"]);
let channel_id = parseInt(json[0]["cid"]);
let permissions = PermissionManager.parse_permission_bulk(json, this);
for(let req of this.requests_client_channel_permissions.slice(0)) {
if(req.client_id == client_id && req.channel_id == channel_id) {
this.requests_client_channel_permissions.remove(req);
req.promise.resolved(permissions);
}
}
this.fullfill_permission_request("requests_client_channel_permissions", {
client_id: client_id,
channel_id: channel_id
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_client_channel_permission_request(request: permissions.PermissionRequestKeys) {
this.handle.serverConnection.send_command("channelclientpermlist", {cldbid: request.client_id, cid: request.channel_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_client_channel_permissions", request, "success", []);
else
this.fullfill_permission_request("requests_client_channel_permissions", request, "error", error);
});
}
requestClientChannelPermissions(client_id: number, channel_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
client_id: client_id
};
return this.execute_permission_request("requests_client_channel_permissions", keys, this.execute_client_channel_permission_request.bind(this));
}
/* playlist permissions */
private onPlaylistPermList(json: any[]) {
let playlist_id = parseInt(json[0]["playlist_id"]);
let permissions = PermissionManager.parse_permission_bulk(json, this);
for(let req of this.requests_playlist_permissions.slice(0)) {
if(req.playlist_id == playlist_id) {
this.requests_playlist_permissions.remove(req);
req.promise.resolved(permissions);
}
this.fullfill_permission_request("requests_playlist_permissions", {
playlist_id: playlist_id
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_playlist_permission_request(request: permissions.PermissionRequestKeys) {
this.handle.serverConnection.send_command("playlistpermlist", {playlist_id: request.playlist_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
this.fullfill_permission_request("requests_playlist_permissions", request, "success", []);
else
this.fullfill_permission_request("requests_playlist_permissions", request, "error", error);
});
}
requestPlaylistPermissions(playlist_id: number) : Promise<PermissionValue[]> {
for(let request of this.requests_playlist_permissions)
if(request.playlist_id == playlist_id && request.promise.time() + 1000 > Date.now())
return request.promise;
const keys: permissions.PermissionRequestKeys = {
playlist_id: playlist_id
};
return this.execute_permission_request("requests_playlist_permissions", keys, this.execute_playlist_permission_request.bind(this));
}
let request: TeaPermissionRequest = {} as any;
request.playlist_id = playlist_id;
request.promise = new LaterPromise<PermissionValue[]>();
/* playlist client permissions */
private onPlaylistClientPermList(json: any[]) {
let playlist_id = parseInt(json[0]["playlist_id"]);
let client_id = parseInt(json[0]["cldbid"]);
this.handle.serverConnection.send_command("playlistpermlist", {playlist_id: playlist_id}).catch(error => {
this.requests_playlist_permissions.remove(request);
this.fullfill_permission_request("requests_playlist_client_permissions", {
playlist_id: playlist_id,
client_id: client_id
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_playlist_client_permission_request(request: permissions.PermissionRequestKeys) {
this.handle.serverConnection.send_command("playlistclientpermlist", {playlist_id: request.playlist_id, cldbid: request.client_id})
.catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT)
request.promise.resolved([]);
this.fullfill_permission_request("requests_playlist_client_permissions", request, "success", []);
else
request.promise.rejected(error);
this.fullfill_permission_request("requests_playlist_client_permissions", request, "error", error);
});
}
this.requests_playlist_permissions.push(request);
requestPlaylistClientPermissions(playlist_id: number, client_database_id: number) : Promise<PermissionValue[]> {
const keys: permissions.PermissionRequestKeys = {
playlist_id: playlist_id,
client_id: client_database_id
};
return this.execute_permission_request("requests_playlist_client_permissions", keys, this.execute_playlist_client_permission_request.bind(this));
}
private readonly criteria_equal = (a, b) => {
for(const criteria of ["client_id", "channel_id", "playlist_id"]) {
if((typeof a[criteria] === "undefined") !== (typeof b[criteria] === "undefined")) return false;
if(a[criteria] != b[criteria]) return false;
}
return true;
};
private execute_permission_request(list: RequestLists,
criteria: permissions.PermissionRequestKeys,
execute: (criteria: permissions.PermissionRequestKeys) => any) : Promise<PermissionValue[]> {
for(const request of this[list])
if(this.criteria_equal(request, criteria) && request.promise.time() + 1000 < Date.now())
return request.promise;
const result = Object.assign({
timeout_id: setTimeout(() => this.fullfill_permission_request(list, criteria, "error", tr("timeout")), 5000),
promise: new LaterPromise<PermissionValue[]>()
}, criteria);
this[list].push(result);
execute(criteria);
return result.promise;
};
private fullfill_permission_request(list: RequestLists, criteria: permissions.PermissionRequestKeys, status: "success" | "error", result: any) {
for(const request of this[list]) {
if(this.criteria_equal(request, criteria)) {
this[list].remove(request);
clearTimeout(request.timeout_id);
status === "error" ? request.promise.rejected(result) : request.promise.resolved(result);
}
}
}
find_permission(...permissions: string[]) : Promise<permissions.find.Entry[]> {
const permission_ids = [];
for(const permission of permissions) {
const info = this.resolveInfo(permission);
if(!info) continue;
permission_ids.push(info.id);
}
if(!permission_ids.length) return Promise.resolve([]);
return new Promise<permissions.find.Entry[]>((resolve, reject) => {
const single_handler = {
command: "notifypermfind",
function: command => {
const result: permissions.find.Entry[] = [];
for(const entry of command.arguments) {
const perm_id = parseInt(entry["p"]);
if(permission_ids.indexOf(perm_id) === -1) return; /* not our permfind result */
const value = parseInt(entry["v"]);
const type = parseInt(entry["t"]);
let data;
switch (type) {
case 0:
data = {
type: "server_group",
group_id: parseInt(entry["id1"]),
} as permissions.find.ServerGroup;
break;
case 1:
data = {
type: "client",
client_id: parseInt(entry["id2"]),
} as permissions.find.Client;
break;
case 2:
data = {
type: "channel",
channel_id: parseInt(entry["id2"]),
} as permissions.find.Channel;
break;
case 3:
data = {
type: "channel_group",
group_id: parseInt(entry["id1"]),
} as permissions.find.ChannelGroup;
break;
case 4:
data = {
type: "client_channel",
client_id: parseInt(entry["id1"]),
channel_id: parseInt(entry["id1"]),
} as permissions.find.ClientChannel;
break;
default:
continue;
}
data.id = perm_id;
data.value = value;
result.push(data);
}
resolve(result);
return true;
}
};
this.handler_boss.register_single_handler(single_handler);
this.connection.send_command("permfind", permission_ids.map(e => { return {permid: e }})).catch(error => {
this.handler_boss.remove_single_handler(single_handler);
if(error instanceof CommandResult && error.id == ErrorID.EMPTY_RESULT) {
resolve([]);
return;
}
reject(error);
});
});
}
neededPermission(key: number | string | PermissionType | PermissionInfo) : NeededPermissionValue {

View File

@ -54,6 +54,7 @@ class ClientProperties {
client_total_bytes_downloaded: number = 0;
client_talk_power: number = 0;
client_is_priority_speaker: boolean = false;
}
class ClientConnectionInfo {
@ -1237,6 +1238,10 @@ class MusicClientProperties extends ClientProperties {
client_playlist_id: number = 0;
client_disabled: boolean = false;
client_flag_notify_song_change: boolean = false;
client_bot_type: number = 0;
client_uptime_mode: number = 0;
}
/*

View File

@ -10,7 +10,12 @@ namespace tooltip {
hide();
update();
}
export function initialize(entry: JQuery) : Handle {
export function initialize(entry: JQuery, callbacks?: {
on_show?(tag: JQuery),
on_hide?(tag: JQuery)
}) : Handle {
if(!callbacks) callbacks = {};
let _show;
let _hide;
let _shown;
@ -34,12 +39,14 @@ namespace tooltip {
_global_tooltip[0].classList.add("shown");
_global_tooltip[0].innerHTML = node_content[0].innerHTML;
callbacks.on_show && callbacks.on_show(_global_tooltip);
_flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */
};
const mouseexit = () => {
if(_global_tooltip) {
if(!_force_show) {
callbacks.on_hide && callbacks.on_hide(_global_tooltip);
_global_tooltip[0].classList.remove("shown");
}
_flag_shown = false;

View File

@ -431,6 +431,7 @@ namespace top_menu {
{
const menu = driver.append_item(tr("Tools"));
/*
item = menu.append_item(tr("Manage Playlists"));
item.icon('client-music');
item.click(() => {
@ -442,6 +443,7 @@ namespace top_menu {
}
});
_state_updater["tools.pl"] = { item: item, conditions: [condition_connected]};
*/
item = menu.append_item(tr("Ban List"));
item.icon('client-ban_list');

View File

@ -65,7 +65,12 @@ namespace chat {
this.handle.show_private_conversations();
})[0];
this._button_bot_manage = this._html_tag.find(".bot-manage");
this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => {
const bot = this.handle.music_info().current_bot();
if(!bot) return;
Modals.openMusicManage(this.handle.handle, bot);
});
this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => {
this.handle.music_info().events.fire("action_song_add");
});

View File

@ -656,6 +656,10 @@ namespace chat {
});
}
current_bot() : MusicClientEntry | undefined {
return this._current_bot;
}
private sort_songs(data: PlaylistSong[]) {
const result = [];

View File

@ -13,7 +13,8 @@ namespace htmltags {
client_id: number,
client_unique_id: string,
client_name: string,
add_braces?: boolean
add_braces?: boolean,
client_database_id?: number; /* not yet used */
}
export interface ChannelProperties {

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
/// <reference path="../../ui/elements/modal.ts" />
/// <reference path="../../ConnectionHandler.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
export function openPlaylistManage(client: ConnectionHandler, playlist: Playlist) {
let modal = createModal({
header: tr(tr("Playlist Manage")),
body: () => $("#tmpl_playlist_manage").renderTag().children(),
footer: null,
width: "",
closeable: false
});
//TODO!
modal.open();
}
}

View File

@ -196,6 +196,7 @@ const loader_javascript = {
"js/ui/modal/ModalInvite.js",
"js/ui/modal/ModalIdentity.js",
"js/ui/modal/ModalBanList.js",
"js/ui/modal/ModalMusicManage.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPoke.js",
"js/ui/modal/ModalKeySelect.js",
@ -362,6 +363,7 @@ const loader_style = {
"css/static/modal-channelinfo.css",
"css/static/modal-clientinfo.css",
"css/static/modal-serverinfo.css",
"css/static/modal-musicmanage.css",
"css/static/modal-serverinfobandwidth.css",
"css/static/modal-identity.css",
"css/static/modal-settings.css",
@ -391,34 +393,6 @@ const loader_style = {
}
};
async function load_templates() {
try {
const response = await $.ajax("templates.html" + loader.get_cache_version());
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
loader.critical_error("Failed to find template tag!");
return;
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
} catch(error) {
loader.critical_error("Failed to find template tag!");
throw "template error";
}
}
/* register tasks */
loader.register_task(loader.Stage.INITIALIZING, {
name: "safari fix",
@ -502,7 +476,12 @@ loader.register_task(loader.Stage.STYLE, {
loader.register_task(loader.Stage.TEMPLATES, {
name: "templates",
function: load_templates,
function: async () => {
await loader.load_templates([
"templates.html",
"templates/music/manage.html"
]);
},
priority: 10
});

View File

@ -473,6 +473,70 @@ namespace loader {
}
}
export async function load_template(path: SourcePath) : Promise<void> {
try {
const response = await $.ajax(path + (cache_tag || ""));
let node = document.createElement("html");
node.innerHTML = response;
let tags: HTMLCollection;
if(node.getElementsByTagName("body").length > 0)
tags = node.getElementsByTagName("body")[0].children;
else
tags = node.children;
let root = document.getElementById("templates");
if(!root) {
loader.critical_error("Failed to find template tag!");
throw "Failed to find template tag";
}
while(tags.length > 0){
let tag = tags.item(0);
root.appendChild(tag);
}
} catch(error) {
let msg;
if('responseText' in error)
msg = error.responseText;
else if(error instanceof Error)
msg = error.message;
loader.critical_error("failed to load template " + script_name(path), msg);
throw "template error";
}
}
export async function load_templates(paths: SourcePath[]) : Promise<void> {
const promises: Promise<void>[] = [];
const errors: {
template: SourcePath,
error: any
}[] = [];
for(const template of paths)
promises.push(load_template(template).catch(error => {
errors.push({
template: template,
error: error
});
return Promise.resolve();
}));
await Promise.all([...promises]);
if(errors.length > 0) {
if (loader.config.error) {
console.error("Failed to load the following templates:");
for (const sheet of errors)
console.log(" - %o: %o", sheet.template, sheet.error);
}
loader.critical_error("Failed to load template " + script_name(errors[0].template) + " <br>" + "View the browser console for more information!");
throw "failed to load template " + script_name(errors[0].template);
}
}
export type ErrorHandler = (message: string, detail: string) => void;
@ -578,7 +642,7 @@ namespace loader {
const display_detect = /./;
display_detect.toString = function() { print_security(); return ""; };
clog("%cLovely to see you using and debugging the TeaSpeak Web client.", css);
clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css);
clog("%cIf you have some good ideas or already done some incredible changes,", css);
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
clog("%c ", display_detect);