Added permission management system

canary
WolverinDEV 2018-09-30 21:50:59 +02:00
parent d9c858315f
commit aad4023ed5
31 changed files with 2654 additions and 224 deletions

View File

@ -1,4 +1,7 @@
# Changelog:
* **30.09.18**
- Added the permission system (Assignments and management)
* **26.09.18**:
- Added Safari support

137
css/context_menu.scss Normal file
View File

@ -0,0 +1,137 @@
.context-menu {
overflow: visible;
display: none;
z-index: 1000;
position: absolute;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
padding: 3px;
* {
font-family: Arial, serif;
font-size: 12px;
white-space: pre;
line-height: 1;
vertical-align: middle;
}
.entry {
/*padding: 8px 12px;*/
padding-right: 12px;
cursor: pointer;
list-style-type: none;
transition: all .3s ease;
user-select: none;
align-items: center;
display: flex;
&.disabled {
background-color: lightgray;
cursor: not-allowed;
}
&:hover:not(.disabled) {
background-color: #DEF;
}
}
.icon_empty, .icon {
margin-right: 4px;
}
.arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 5px;
position: absolute;
right: 3px;
}
.sub-container {
padding-right: 3px;
position: relative;
&:hover {
.sub-menu {
display: block;
}
}
}
.sub-menu {
display: none;
left: 100%;
top: -4px;
position: absolute;
margin-left: 3px;
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}
}

View File

@ -4,53 +4,6 @@
box-sizing: border-box;
}
.contextMenu {
display: none;
z-index: 1000;
position: absolute;
overflow: hidden;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
padding: 3px;
}
.contextMenu *{
font-family: Arial;
font-size: 12px;
white-space: pre;
line-height: 1;
vertical-align: middle;
}
/* Each of the items in the list */
.contextMenu li {
/*padding: 8px 12px;*/
padding-right: 12px;
cursor: pointer;
list-style-type: none;
transition: all .3s ease;
user-select: none;
align-items: center;
display: flex;
}
.contextMenu .icon_empty,
.contextMenu .icon {
margin-right: 4px;
}
.contextMenu li:hover:not(.disabled) {
background-color: #DEF;
}
.contextMenu li.disabled {
background-color: lightgray;
cursor: not-allowed;
}
.align_row {
display: flex;
flex-direction: row;

View File

@ -85,4 +85,281 @@
.settings_advanced .group_box fieldset, .settings_advanced .group_box fieldset > div {
width: 100%; }
.permission-explorer {
width: 100%;
display: grid;
grid-template-rows: min-content auto;
grid-gap: 5px; }
.permission-explorer .bar-filter {
display: grid;
grid-gap: 5px;
grid-template-columns: max-content auto max-content; }
.permission-explorer .bar-filter input[type="text"] {
width: 100%; }
.permission-explorer.disabled {
pointer-events: none; }
.permission-explorer.disabled .overlay-disabled {
display: block; }
.permission-explorer.disabled input {
background-color: #00000033; }
.permission-explorer .overlay-disabled {
display: none;
position: absolute;
background-color: #00000033;
z-index: 1000;
height: 100%;
width: 100%; }
.permission-explorer .list {
display: flex;
position: relative;
flex-direction: column;
border: lightgray solid 2px;
user-select: none;
padding-bottom: 2px;
overflow-y: scroll;
overflow-x: hidden; }
.permission-explorer .list .header {
position: sticky;
top: 0;
z-index: 1;
background-color: lightgray;
padding-left: 0 !important; }
.permission-explorer .list .header > div {
border: grey solid;
border-width: 0 2px 0 0;
padding-left: 2px; }
.permission-explorer .list .header > div:last-of-type {
border: none; }
.permission-explorer .list > .entry {
padding-left: 4px; }
.permission-explorer .list .entry {
display: grid;
grid-template-columns: auto 100px 100px 100px 100px; }
.permission-explorer .list .entry > div {
padding-left: 2px; }
.permission-explorer .list .entry.selected {
background-color: #11111122; }
.permission-explorer .list .entry.unset > .permission-value, .permission-explorer .list .entry.unset > .permission-skip, .permission-explorer .list .entry.unset > .permission-negate {
visibility: hidden; }
.permission-explorer .list .entry.unset > .permission-name {
color: lightgray; }
.permission-explorer .list .group {
grid-template-columns: auto;
grid-template-rows: auto auto; }
.permission-explorer .list .group .group-entries {
margin-left: 50px; }
.permission-explorer .list .group .title.selected {
background-color: #11111122; }
.permission-explorer .list .arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 3px; }
.permission-explorer .list input {
border: none;
background: transparent;
vertical-align: text-bottom;
max-width: 90%; }
.permission-explorer .list .checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */ }
.permission-explorer .list .checkbox input {
position: absolute;
opacity: 0;
cursor: pointer; }
.permission-explorer .list .checkbox .checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee; }
.permission-explorer .list .checkbox .checkmark:after {
content: "";
position: absolute;
display: none;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg); }
.permission-explorer .list .checkbox:hover input ~ .checkmark {
background-color: #ccc; }
.permission-explorer .list .checkbox input:checked ~ .checkmark {
background-color: #2196F3; }
.permission-explorer .list .checkbox input:checked ~ .checkmark:after {
display: block; }
.arrow {
display: inline-block;
border: solid black;
border-width: 0 3px 3px 0;
padding: 3px; }
.arrow.right {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
.arrow.left {
transform: rotate(135deg);
-webkit-transform: rotate(135deg); }
.arrow.up {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg); }
.arrow.down {
transform: rotate(45deg);
-webkit-transform: rotate(45deg); }
.layout-group-server, .layout-group-channel, .layout-channel, .layout-client, .layout-client-channel {
width: 100%;
display: flex;
flex-direction: row; }
.layout-group-server > div, .layout-group-channel > div, .layout-channel > div, .layout-client > div, .layout-client-channel > div {
margin: 5px; }
.layout-group-server .list-group-server, .layout-group-server .list-group-channel, .layout-group-server .list-group-server-clients, .layout-group-server .list-channel, .layout-group-channel .list-group-server, .layout-group-channel .list-group-channel, .layout-group-channel .list-group-server-clients, .layout-group-channel .list-channel, .layout-channel .list-group-server, .layout-channel .list-group-channel, .layout-channel .list-group-server-clients, .layout-channel .list-channel, .layout-client .list-group-server, .layout-client .list-group-channel, .layout-client .list-group-server-clients, .layout-client .list-channel, .layout-client-channel .list-group-server, .layout-client-channel .list-group-channel, .layout-client-channel .list-group-server-clients, .layout-client-channel .list-channel {
position: relative; }
.layout-group-server .list-group-server .entries, .layout-group-server .list-group-channel .entries, .layout-group-server .list-group-server-clients .entries, .layout-group-server .list-channel .entries, .layout-group-channel .list-group-server .entries, .layout-group-channel .list-group-channel .entries, .layout-group-channel .list-group-server-clients .entries, .layout-group-channel .list-channel .entries, .layout-channel .list-group-server .entries, .layout-channel .list-group-channel .entries, .layout-channel .list-group-server-clients .entries, .layout-channel .list-channel .entries, .layout-client .list-group-server .entries, .layout-client .list-group-channel .entries, .layout-client .list-group-server-clients .entries, .layout-client .list-channel .entries, .layout-client-channel .list-group-server .entries, .layout-client-channel .list-group-channel .entries, .layout-client-channel .list-group-server-clients .entries, .layout-client-channel .list-channel .entries {
display: table;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
min-width: 100%; }
.layout-group-server .list-group-server, .layout-group-server .list-group-channel, .layout-group-channel .list-group-server, .layout-group-channel .list-group-channel, .layout-channel .list-group-server, .layout-channel .list-group-channel, .layout-client .list-group-server, .layout-client .list-group-channel, .layout-client-channel .list-group-server, .layout-client-channel .list-group-channel {
width: 300px;
flex-grow: 0;
border: grey solid 1px;
user-select: none;
overflow: auto;
position: relative; }
.layout-group-server .list-group-server .group, .layout-group-server .list-group-channel .group, .layout-group-channel .list-group-server .group, .layout-group-channel .list-group-channel .group, .layout-channel .list-group-server .group, .layout-channel .list-group-channel .group, .layout-client .list-group-server .group, .layout-client .list-group-channel .group, .layout-client-channel .list-group-server .group, .layout-client-channel .list-group-channel .group {
display: block;
white-space: nowrap;
cursor: pointer; }
.layout-group-server .list-group-server .group .icon, .layout-group-server .list-group-server .group .icon_empty, .layout-group-server .list-group-channel .group .icon, .layout-group-server .list-group-channel .group .icon_empty, .layout-group-channel .list-group-server .group .icon, .layout-group-channel .list-group-server .group .icon_empty, .layout-group-channel .list-group-channel .group .icon, .layout-group-channel .list-group-channel .group .icon_empty, .layout-channel .list-group-server .group .icon, .layout-channel .list-group-server .group .icon_empty, .layout-channel .list-group-channel .group .icon, .layout-channel .list-group-channel .group .icon_empty, .layout-client .list-group-server .group .icon, .layout-client .list-group-server .group .icon_empty, .layout-client .list-group-channel .group .icon, .layout-client .list-group-channel .group .icon_empty, .layout-client-channel .list-group-server .group .icon, .layout-client-channel .list-group-server .group .icon_empty, .layout-client-channel .list-group-channel .group .icon, .layout-client-channel .list-group-channel .group .icon_empty {
margin-right: 3px; }
.layout-group-server .list-group-server .group .name.savedb, .layout-group-server .list-group-channel .group .name.savedb, .layout-group-channel .list-group-server .group .name.savedb, .layout-group-channel .list-group-channel .group .name.savedb, .layout-channel .list-group-server .group .name.savedb, .layout-channel .list-group-channel .group .name.savedb, .layout-client .list-group-server .group .name.savedb, .layout-client .list-group-channel .group .name.savedb, .layout-client-channel .list-group-server .group .name.savedb, .layout-client-channel .list-group-channel .group .name.savedb {
color: blue; }
.layout-group-server .list-group-server .group .name.default, .layout-group-server .list-group-channel .group .name.default, .layout-group-channel .list-group-server .group .name.default, .layout-group-channel .list-group-channel .group .name.default, .layout-channel .list-group-server .group .name.default, .layout-channel .list-group-channel .group .name.default, .layout-client .list-group-server .group .name.default, .layout-client .list-group-channel .group .name.default, .layout-client-channel .list-group-server .group .name.default, .layout-client-channel .list-group-channel .group .name.default {
color: black;
font-weight: bold; }
.layout-group-server .list-group-server .group.selected, .layout-group-server .list-group-channel .group.selected, .layout-group-channel .list-group-server .group.selected, .layout-group-channel .list-group-channel .group.selected, .layout-channel .list-group-server .group.selected, .layout-channel .list-group-channel .group.selected, .layout-client .list-group-server .group.selected, .layout-client .list-group-channel .group.selected, .layout-client-channel .list-group-server .group.selected, .layout-client-channel .list-group-channel .group.selected {
background-color: blue; }
.layout-group-server .list-group-server .group.selected .name.savedb, .layout-group-server .list-group-channel .group.selected .name.savedb, .layout-group-channel .list-group-server .group.selected .name.savedb, .layout-group-channel .list-group-channel .group.selected .name.savedb, .layout-channel .list-group-server .group.selected .name.savedb, .layout-channel .list-group-channel .group.selected .name.savedb, .layout-client .list-group-server .group.selected .name.savedb, .layout-client .list-group-channel .group.selected .name.savedb, .layout-client-channel .list-group-server .group.selected .name.savedb, .layout-client-channel .list-group-channel .group.selected .name.savedb {
color: white; }
.layout-group-server .permission-explorer {
flex-grow: 70; }
.layout-group-server .list-group-server-clients {
flex-grow: 0;
width: 200px;
border: grey solid 1px; }
.layout-channel .list-channel, .layout-client-channel .list-channel {
display: flex;
flex-direction: column;
overflow-x: scroll;
overflow-y: auto;
width: 300px;
flex-grow: 1; }
.layout-channel .list-channel .channel, .layout-client-channel .list-channel .channel {
cursor: pointer;
display: block;
width: 100%;
height: max-content;
white-space: nowrap; }
.layout-channel .list-channel .channel .icon, .layout-channel .list-channel .channel .icon_empty, .layout-client-channel .list-channel .channel .icon, .layout-client-channel .list-channel .channel .icon_empty {
margin-right: 3px; }
.layout-channel .list-channel .channel.selected, .layout-client-channel .list-channel .channel.selected {
background-color: blue; }
.layout-client .client-info, .layout-client-channel .client-info {
display: flex;
flex-direction: column; }
.layout-client .client-info > div:not(.list-channel), .layout-client-channel .client-info > div:not(.list-channel) {
display: grid;
grid-template-columns: auto;
grid-template-rows: max-content; }
.layout-client .client-info .client-info input, .layout-client-channel .client-info .client-info input {
pointer-events: none; }
.group-assignment-list .group-list {
border: lightgray solid 1px;
padding: 3px; }
.group-assignment-list .group-list .group-entry {
display: flex;
flex-direction: row;
height: max-content; }
.group-assignment-list .group-list .checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 18px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */ }
.group-assignment-list .group-list .checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none; }
.group-assignment-list .group-list .checkbox .checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee;
margin-right: 4px; }
.group-assignment-list .group-list .checkbox .checkmark:after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg); }
.group-assignment-list .group-list .checkbox:hover:not(.disabled) input ~ .checkmark {
background-color: #ccc; }
.group-assignment-list .group-list .checkbox input:checked ~ .checkmark {
background-color: #2196F3; }
.group-assignment-list .group-list .checkbox input:checked ~ .checkmark:after {
display: block; }
.group-assignment-list .group-list .checkbox.disabled {
user-select: none;
pointer-events: none;
cursor: not-allowed; }
.group-assignment-list .group-list .checkbox.disabled .checkmark {
background-color: #00000055; }
.group-assignment-list .group-list .checkbox.disabled .checkmark:after {
border-color: #00000055; }
/*# sourceMappingURL=modals.css.map */

File diff suppressed because one or more lines are too long

View File

@ -138,4 +138,421 @@
width: 100%;
}
}
}
.permission-explorer {
width: 100%;
display: grid;
grid-template-rows: min-content auto;
grid-gap: 5px;
.bar-filter {
display: grid;
grid-gap: 5px;
grid-template-columns: max-content auto max-content;
input[type="text"] {
width: 100%;
}
}
&.disabled {
pointer-events: none;
.overlay-disabled {
display: block;
}
input {
background-color: #00000033;
}
}
.overlay-disabled {
display: none;
position: absolute;
background-color: #00000033;
z-index: 1000;
height: 100%;
width: 100%;
}
.list {
display: flex;
position: relative;
flex-direction: column;
border: lightgray solid 2px;
user-select: none;
padding-bottom: 2px;
overflow-y: scroll;
overflow-x: hidden;
.header {
position: sticky;
top: 0;
z-index: 1;
background-color: lightgray;
padding-left: 0!important;
& > div {
border: grey solid;
border-width: 0 2px 0 0;
padding-left: 2px;
}
& > div:last-of-type {
border: none;
}
}
& > .entry {
padding-left: 4px;
}
.entry {
display: grid;
grid-template-columns: auto 100px 100px 100px 100px;
& > div {
padding-left: 2px;
}
&.selected {
background-color: #11111122;
}
&.unset {
& > .permission-value, & > .permission-skip, & > .permission-negate {
visibility: hidden;
}
& > .permission-name {
color: lightgray;
}
}
}
.group {
grid-template-columns: auto;
grid-template-rows: auto auto;
.group-entries {
margin-left: 50px;
}
.title {
&.selected {
background-color: #11111122;
}
}
}
.arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 3px;
}
input {
border: none;
background: transparent;
vertical-align: text-bottom;
max-width: 90%;
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}
}
}
.arrow {
display: inline-block;
border: solid black;
border-width: 0 3px 3px 0;
padding: 3px;
&.right {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
&.left {
transform: rotate(135deg);
-webkit-transform: rotate(135deg);
}
&.up {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
}
&.down {
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
}
.layout-group-server, .layout-group-channel, .layout-channel, .layout-client, .layout-client-channel {
width: 100%;
display: flex;
flex-direction: row;
& > div {
margin: 5px;
}
.list-group-server, .list-group-channel, .list-group-server-clients, .list-channel {
position: relative;
.entries {
display: table;
position: absolute;
top: 0; bottom: 0;
left: 0; right: 0;
min-width: 100%;
}
}
.list-group-server, .list-group-channel {
width: 300px;
flex-grow: 0;
border: grey solid 1px;
user-select: none;
overflow: auto;
position: relative;
.group {
display: block;
white-space: nowrap;
cursor: pointer;
.icon, .icon_empty {
margin-right: 3px;
}
.name.savedb {
color: blue;
}
.name.default {
color: black;
font-weight: bold;
}
&.selected {
background-color: blue;
.name.savedb {
color: white;
}
}
}
}
}
.layout-group-server {
.list-group-server { }
.permission-explorer {
flex-grow: 70;
}
.list-group-server-clients {
flex-grow: 0;
width: 200px;
border: grey solid 1px;
}
}
.layout-channel, .layout-client-channel {
.list-channel {
display: flex;
flex-direction: column;
overflow-x: scroll;
overflow-y: auto;
width: 300px;
flex-grow: 1;
.channel {
cursor: pointer;
display: block;
width: 100%;
height: max-content;
white-space: nowrap;
.icon, .icon_empty {
margin-right: 3px;
}
&.selected {
background-color: blue;
}
}
}
}
.layout-client, .layout-client-channel {
.client-info {
display: flex;
flex-direction: column;
& > div:not(.list-channel) {
display: grid;
grid-template-columns: auto;
grid-template-rows: max-content;
}
.client-info {
input {
pointer-events: none;
}
}
}
}
.group-assignment-list {
.group-list {
border: lightgray solid 1px;
padding: 3px;
.group-entry {
display: flex;
flex-direction: row;
height: max-content;
}
.checkbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 18px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
background-color: #eee;
margin-right: 4px;
&:after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover:not(.disabled) input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
&.disabled {
user-select: none;
pointer-events: none;
cursor: not-allowed;
.checkmark {
background-color: #00000055;
&:after {
border-color: #00000055;
}
}
}
}
}
}

View File

@ -16,7 +16,7 @@ x-tab { display:none }
border-radius: 0px 2px 2px 2px;
border-style: solid;
overflow-y: auto;
height: calc(100% - 2 * 18px);
height: 100%;
padding: 2px;
}
@ -68,6 +68,6 @@ x-tab { display:none }
display: none;
}
.tab .selected {
background: #11111111!important;
.tab .tab-header .entry.selected {
background: #11111111;
}

View File

@ -42,6 +42,7 @@
<link rel="stylesheet" href="css/music/info_plate.css" type="text/css">
<link rel="stylesheet" href="css/frame/SelectInfo.css" type="text/css">
<link rel="stylesheet" href="css/control_bar.css" type="text/css">
<link rel="stylesheet" href="css/context_menu.css" type="text/css">
<link rel="stylesheet" href="vendor/bbcode/xbbcode.css" type="text/css">
<!-- https://localhost:9987/?forward_url=http%3A%2F%2Flocalhost%3A63344%2FWeb-Client%2Findex.php%3F_ijt%3D82b1uhmnh0a5l1n35nnjps5eid%26loader_ignore_age%3D1%26connect_default_host%3Dlocalhost%26default_connect_type%3Dforum%26default_connect_url%3Dtrue%26default_connect_type%3Dteamspeak%26default_connect_url%3Dlocalhost%253A9987 -->
@ -214,7 +215,7 @@
</div>
</div>
</div>
<div id="contextMenu" class="contextMenu"></div>
<div id="contextMenu" class="context-menu"></div>
<!--
<div style="background-color:white;">

View File

@ -321,7 +321,7 @@ class IconManager {
}
//$("<img width=\"16\" height=\"16\" alt=\"tick\" src=\"data:image/png;base64," + value.base64 + "\">")
generateTag(id: number) {
generateTag(id: number) : JQuery<HTMLDivElement> {
if(id == 0)
return $("<div class='icon_empty'></div>");
else if(id < 1000)

View File

@ -43,6 +43,9 @@ namespace TSIdentityHelper {
export function unwarpString(str) : string {
if(str == "") return "";
try {
if(!$.isFunction(window.Pointer_stringify) || !$.isFunction(Pointer_stringify)) {
displayCriticalError("Missing required wasm function!<br>Please reload the page!", false);
}
let message: string = Pointer_stringify(str);
functionDestroyString(str);
return message;

View File

@ -225,7 +225,7 @@ class ChatEntry {
//TODO Implement this?
}
});
spawnMenu(e.pageX, e.pageY, ...actions);
spawn_context_menu(e.pageX, e.pageY, ...actions);
});
closeTag.click(function () {

View File

@ -37,6 +37,7 @@ class ServerConnection {
_handshakeHandler: HandshakeHandler;
commandHandler: ConnectionCommandHandler;
readonly helper: CommandHelper;
private _connectTimeoutHandler: NodeJS.Timer = undefined;
private _connected: boolean = false;
private _retCodeIdx: number;
@ -47,6 +48,7 @@ class ServerConnection {
this._socket = null;
this.commandHandler = new ConnectionCommandHandler(this);
this.helper = new CommandHelper(this);
this._retCodeIdx = 0;
this._retListener = [];
}
@ -351,6 +353,73 @@ class HandshakeHandler {
}
}
interface ClientNameInfo {
//cluid=tYzKUryn\/\/Y8VBMf8PHUT6B1eiE= name=Exp clname=Exp cldbid=9
client_unique_id: string;
client_nickname: string;
client_database_id: number;
}
interface ClientNameFromUid {
promise: LaterPromise<ClientNameInfo[]>,
keys: string[],
response: ClientNameInfo[]
}
class CommandHelper {
readonly connection: ServerConnection;
private _callbacks_namefromuid: ClientNameFromUid[] = [];
constructor(connection) {
this.connection = connection;
this.connection.commandHandler["notifyclientnamefromuid"] = this.handle_notifyclientnamefromuid.bind(this);
}
info_from_uid(...uid: string[]) : Promise<ClientNameInfo[]> {
let uids = [...uid];
for(let p of this._callbacks_namefromuid)
if(p.keys == uids) return p.promise;
let req: ClientNameFromUid = {} as any;
req.keys = uids;
req.response = new Array(uids.length);
req.promise = new LaterPromise<ClientNameInfo[]>();
for(let uid of uids) {
this.connection.sendCommand("clientgetnamefromuid", {
cluid: uid
}).catch(req.promise.function_rejected());
}
this._callbacks_namefromuid.push(req);
return req.promise;
}
private handle_notifyclientnamefromuid(json: any[]) {
for(let entry of json) {
let info: ClientNameInfo = {} as any;
info.client_unique_id = entry["cluid"];
info.client_nickname = entry["clname"];
info.client_database_id = parseInt(entry["cldbid"]);
for(let elm of this._callbacks_namefromuid.slice(0)) {
let unset = 0;
for(let index = 0; index < elm.keys.length; index++) {
if(elm.keys[index] == info.client_unique_id) {
elm.response[index] = info;
}
if(elm.response[index] == undefined) unset++;
}
if(unset == 0) {
this._callbacks_namefromuid.remove(elm);
elm.promise.resolved(elm.response);
}
}
}
}
}
class ConnectionCommandHandler {
readonly connection: ServerConnection;

View File

@ -1,17 +1,14 @@
// If the document is clicked somewhere
$(document).bind("mousedown", function (e) {
// If the clicked element is not the menu
if ($(e.target).parents(".contextMenu").length == 0) {
// Hide it
despawnContextMenu();
if ($(e.target).parents(".context-menu").length == 0) {
despawn_context_menu();
}
});
let contextMenuCloseFn = undefined;
function despawnContextMenu() {
let menue = $(".contextMenu");
if(!menue.is(":visible")) return;
menue.hide(100);
function despawn_context_menu() {
let menu = $(".context-menu");
if(!menu.is(":visible")) return;
menu.hide(100);
if(contextMenuCloseFn) contextMenuCloseFn();
}
@ -19,7 +16,7 @@ enum MenuEntryType {
CLOSE,
ENTRY,
HR,
EMPTY
SUB_MENU
}
class MenuEntry {
@ -32,65 +29,84 @@ class MenuEntry {
};
};
static EMPTY() {
return {
callback: () => {},
type: MenuEntryType.EMPTY,
name: "",
icon: ""
};
};
static CLOSE(callback: () => void) {
return {
callback: callback,
type: MenuEntryType.EMPTY,
type: MenuEntryType.CLOSE,
name: "",
icon: ""
};
}
}
function spawnMenu(x, y, ...entries: {
callback: () => void;
interface ContextMenuEntry {
callback?: () => void;
type: MenuEntryType;
name: (() => string) | string;
icon: (() => string) | string;
icon?: (() => string) | string | JQuery;
disabled?: boolean;
invalidPermission?: boolean;
}[]) {
const menu = $("#contextMenu");
menu.empty();
menu.hide();
sub_menu?: ContextMenuEntry[];
}
function generate_tag(entry: ContextMenuEntry) : JQuery {
if(entry.type == MenuEntryType.HR) {
return $.spawn("hr");
} else if(entry.type == MenuEntryType.ENTRY) {
console.log(entry.icon);
let icon = $.isFunction(entry.icon) ? entry.icon() : entry.icon;
if(typeof(icon) === "string") {
if(!icon || icon.length == 0) icon = "icon_empty";
else icon = "icon " + icon;
}
let tag = $.spawn("div").addClass("entry");
tag.append(typeof(icon) === "string" ? $.spawn("div").addClass(icon) : icon);
tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name));
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
else {
tag.click(function () {
if($.isFunction(entry.callback)) entry.callback();
despawn_context_menu();
});
}
return tag;
} else if(entry.type == MenuEntryType.SUB_MENU) {
let icon = $.isFunction(entry.icon) ? entry.icon() : entry.icon;
if(typeof(icon) === "string") {
if(!icon || icon.length == 0) icon = "icon_empty";
else icon = "icon " + icon;
}
let tag = $.spawn("div").addClass("entry").addClass("sub-container");
tag.append(typeof(icon) === "string" ? $.spawn("div").addClass(icon) : icon);
tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name));
tag.append($.spawn("div").addClass("arrow right"));
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
else {
let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu");
for(let e of entry.sub_menu)
menu.append(generate_tag(e));
menu.appendTo(tag);
}
return tag;
}
return $.spawn("div").text("undefined");
}
function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
const menu = $("#contextMenu").finish().empty();
contextMenuCloseFn = undefined;
let index = 0;
for(let entry of entries){
if(entry.type == MenuEntryType.HR) {
menu.append("<hr>");
} else if(entry.type == MenuEntryType.CLOSE) {
if(entry.type == MenuEntryType.CLOSE) {
contextMenuCloseFn = entry.callback;
} else if(entry.type == MenuEntryType.ENTRY) {
let icon = $.isFunction(entry.icon) ? entry.icon() : entry.icon;
if(icon.length == 0) icon = "icon_empty";
else icon = "icon " + icon;
let tag = $.spawn("li");
tag.append("<div class='" + icon + "'></div>");
tag.append("<div>" + ($.isFunction(entry.name) ? entry.name() : entry.name) + "</div>");
menu.append(tag);
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
else {
tag.click(function () {
if($.isFunction(entry.callback)) entry.callback();
despawnContextMenu();
});
}
}
} else
menu.append(generate_tag(entry));
}
menu.show(100);

View File

@ -159,6 +159,8 @@ function loadDebug() {
"js/ui/modal/ModalChangeVolume.js",
"js/ui/modal/ModalBanClient.js",
"js/ui/modal/ModalYesNo.js",
"js/ui/modal/ModalPermissionEdit.js",
"js/ui/modal/ModalServerGroupDialog.js",
"js/ui/channel.js",
"js/ui/client.js",
@ -263,13 +265,6 @@ function loadTemplates() {
});
}
interface Navigator {
browserSpecs: {
name: string,
version: string
}
}
navigator.browserSpecs = (function(){
let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if(/trident/i.test(M[1])){

View File

@ -87,6 +87,8 @@ function main() {
console.log("Response: " + flag);
})
*/
//Modals.spawnPermissionEdit();
}
app.loadedListener.push(() => {

View File

@ -18,6 +18,11 @@ class GroupProperties {
namemode: number = 0;
}
class GroupPermissionRequest {
group_id: number;
promise: LaterPromise<PermissionValue[]>;
}
class Group {
properties: GroupProperties = new GroupProperties();
@ -31,6 +36,7 @@ class Group {
requiredMemberAddPower: number = 0;
requiredMemberRemovePower: number = 0;
constructor(handle: GroupManager, id: number, target: GroupTarget, type: GroupType, name: string) {
this.handle = handle;
this.id = id;
@ -58,11 +64,15 @@ class GroupManager {
serverGroups: Group[] = [];
channelGroups: Group[] = [];
private requests_group_permissions: GroupPermissionRequest[] = [];
constructor(client: TSClient) {
this.handle = client;
this.handle.serverConnection.commandHandler["notifyservergrouplist"] = this.onServerGroupList.bind(this);
this.handle.serverConnection.commandHandler["notifychannelgrouplist"] = this.onServerGroupList.bind(this);
this.handle.serverConnection.commandHandler["notifyservergrouppermlist"] = this.onPermissionList.bind(this);
this.handle.serverConnection.commandHandler["notifychannelgrouppermlist"] = this.onPermissionList.bind(this);
}
requestGroups(){
@ -145,4 +155,47 @@ class GroupManager {
console.log("Got " + json.length + " new " + target + " groups:");
}
request_permissions(group: Group) : Promise<PermissionValue[]> { //database_empty_result
for(let request of this.requests_group_permissions)
if(request.group_id == group.id && request.promise.time() + 1000 > Date.now())
return request.promise;
let req = new GroupPermissionRequest();
req.group_id = group.id;
req.promise = new LaterPromise<PermissionValue[]>();
this.requests_group_permissions.push(req);
this.handle.serverConnection.sendCommand(group.target == GroupTarget.SERVER ? "servergrouppermlist" : "channelgrouppermlist", {
cgid: group.id,
sgid: group.id
}).catch(error => {
if(error instanceof CommandResult && error.id == 0x0501)
req.promise.resolved([]);
else
req.promise.rejected(error);
});
return req.promise;
}
private onPermissionList(json: any[]) {
let group = json[0]["sgid"] ? this.serverGroup(parseInt(json[0]["sgid"])) : this.channelGroup(parseInt(json[0]["cgid"]));
if(!group) {
log.error(LogCategory.PERMISSIONS, "Got group permissions for group %o/%o, but its not a registered group!", json[0]["sgid"], json[0]["cgid"]);
return;
}
let requests: GroupPermissionRequest[] = [];
for(let req of this.requests_group_permissions)
if(req.group_id == group.id)
requests.push(req);
if(requests.length == 0) {
log.warn(LogCategory.PERMISSIONS, "Got group permissions for group %o/%o, but it was never requested!", json[0]["sgid"], json[0]["cgid"]);
return;
}
let permissions = PermissionManager.parse_permission_bulk(json, this.handle.permissions);
for(let req of requests) {
this.requests_group_permissions.remove(req);
req.promise.resolved(permissions);
}
}
}

View File

@ -294,9 +294,26 @@ class PermissionInfo {
description: string;
}
class PermissionGroup {
begin: number;
end: number;
deep: number;
name: string;
}
class GroupedPermissions {
group: PermissionGroup;
permissions: PermissionInfo[];
children: GroupedPermissions[];
parent: GroupedPermissions;
}
class PermissionValue {
readonly type: PermissionInfo;
value: number;
flag_skip: boolean;
flag_negate: boolean;
granted_value: number;
constructor(type, value) {
this.type = type;
@ -330,23 +347,103 @@ class ChannelPermissionRequest {
callback_error: ((_: any) => any)[] = [];
}
class TeaPermissionRequest {
client_id?: number;
channel_id?: number;
promise: LaterPromise<PermissionValue[]>;
}
class PermissionManager {
readonly handle: TSClient;
permissionList: PermissionInfo[] = [];
permissionGroups: PermissionGroup[] = [];
neededPermissions: NeededPermissionValue[] = [];
requests_channel_permissions: ChannelPermissionRequest[] = [];
requests_client_permissions: TeaPermissionRequest[] = [];
requests_client_channel_permissions: TeaPermissionRequest[] = [];
initializedListener: ((initialized: boolean) => void)[] = [];
private _cacheNeededPermissions: any;
/* Static info mapping until TeaSpeak implements a detailed info */
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},
//TODO Music bot
{name: "File Transfer", deep: 0},
];
private _group_mapping;
public static parse_permission_bulk(json: any[], manager: PermissionManager) : PermissionValue[] {
let permissions: PermissionValue[] = [];
for(let perm of json) {
let perm_id = parseInt(perm["permid"]);
let perm_grant = (perm_id & (1 << 15)) > 0;
if(perm_grant)
perm_id &= ~(1 << 15);
let perm_info = manager.resolveInfo(perm_id);
if(!perm_info) {
log.warn(LogCategory.PERMISSIONS, "Got unknown permission id (%o/%o (%o))!", perm["permid"], perm_id, perm["permsid"]);
return;
}
let permission: PermissionValue;
for(let ref_perm of permissions) {
if(ref_perm.type == perm_info) {
permission = ref_perm;
break;
}
}
if(!permission) {
permission = new PermissionValue(perm_info, 0);
permission.granted_value = undefined;
permission.value = undefined;
permissions.push(permission);
}
if(perm_grant) {
permission.granted_value = parseInt(perm["permvalue"]);
} else {
permission.value = parseInt(perm["permvalue"]);
permission.flag_negate = perm["permnegated"] == "1";
permission.flag_skip = perm["permskip"] == "1";
}
}
return permissions;
}
constructor(client: TSClient) {
this.handle = client;
this.handle.serverConnection.commandHandler["notifyclientneededpermissions"] = this.onNeededPermissions.bind(this);
this.handle.serverConnection.commandHandler["notifypermissionlist"] = this.onPermissionList.bind(this);
this.handle.serverConnection.commandHandler["notifychannelpermlist"] = this.onChannelPermList.bind(this);
this.handle.serverConnection.commandHandler["notifyclientpermlist"] = this.onClientPermList.bind(this);
}
initialized() : boolean {
@ -359,10 +456,25 @@ class PermissionManager {
private onPermissionList(json) {
this.permissionList = [];
this.permissionGroups = [];
this._group_mapping = PermissionManager.group_mapping.slice();
let group = log.group(log.LogType.TRACE, LogCategory.PERMISSIONS, "Permission mapping");
for(let e of json) {
if(e["group_id_end"]) continue; //Skip all group ids (may use later?)
if(e["group_id_end"]) {
let group = new PermissionGroup();
group.begin = this.permissionGroups.length ? this.permissionGroups.last().end : 0;
group.end = parseInt(e["group_id_end"]);
group.deep = 0;
group.name = "Group " + e["group_id_end"];
let info = this._group_mapping.pop_front();
if(info) {
group.name = info.name;
group.deep = info.deep;
}
this.permissionGroups.push(group);
}
let perm = new PermissionInfo();
perm.name = e["permname"];
@ -430,19 +542,9 @@ class PermissionManager {
}
private onChannelPermList(json) {
let permissions: PermissionValue[] = [];
let channelId: number = parseInt(json[0]["cid"]);
for(let element of json) {
let permission = this.resolveInfo(element["permid"]);
//TODO granted skipped and negated permissions
if(!permission) {
log.error(LogCategory.PERMISSIONS, "Failed to parse channel permission with id %o", element["permid"]);
continue;
}
permissions.push(new PermissionValue(permission, element["permvalue"]));
}
let permissions = PermissionManager.parse_permission_bulk(json, this.handle.permissions);
log.debug(LogCategory.PERMISSIONS, "Got channel permissions for channel %o", channelId);
for(let element of this.requests_channel_permissions) {
if(element.channel_id == channelId) {
@ -466,7 +568,7 @@ class PermissionManager {
return new Promise<PermissionValue[]>((resolve, reject) => {
let request: ChannelPermissionRequest;
for(let element of this.requests_channel_permissions)
if(element.requested + 1000 < Date.now() && request.channel_id == channelId) {
if(element.requested + 1000 < Date.now() && element.channel_id == channelId) {
request = element;
break;
}
@ -482,6 +584,58 @@ class PermissionManager {
});
}
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.sendCommand("clientpermlist", {cldbid: client_id}).catch(error => {
if(error instanceof CommandResult && error.id == 0x0501)
request.promise.resolved([]);
else
request.promise.rejected(error);
});
this.requests_client_permissions.push(request);
return request.promise;
}
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.sendCommand("channelclientpermlist", {cldbid: client_id, cid: channel_id}).catch(error => {
if(error instanceof CommandResult && error.id == 0x0501)
request.promise.resolved([]);
else
request.promise.rejected(error);
});
this.requests_client_channel_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);
}
}
}
neededPermission(key: number | string | PermissionType | PermissionInfo) : PermissionValue {
for(let perm of this.neededPermissions)
if(perm.type.id == key || perm.type.name == key || perm.type == key)
@ -495,6 +649,43 @@ class PermissionManager {
let result = new NeededPermissionValue(info, -2);
this.neededPermissions.push(result);
return result;
}
groupedPermissions() : GroupedPermissions[] {
let result: GroupedPermissions[] = [];
let current: GroupedPermissions;
for(let group of this.permissionGroups) {
if(group.deep == 0) {
current = new GroupedPermissions();
current.group = group;
current.parent = undefined;
current.children = [];
current.permissions = [];
result.push(current);
} else {
if(!current) {
throw "invalid order!";
} else {
while(group.deep <= current.group.deep)
current = current.parent;
let parent = current;
current = new GroupedPermissions();
current.group = group;
current.parent = parent;
current.children = [];
current.permissions = [];
parent.children.push(current);
}
}
for(let permission of this.permissionList)
if(permission.id > current.group.begin && permission.id <= current.group.end)
current.permissions.push(permission);
}
return result;
}
}

View File

@ -26,29 +26,29 @@ interface String {
}
if(!JSON.map_to) {
JSON.map_to = function<T>(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number) : T {
if(!validator) validator = (a, b) => true;
JSON.map_to = function <T>(object: T, json: any, variables?: string | string[], validator?: (map_field: string, map_value: string) => boolean, variable_direction?: number): T {
if (!validator) validator = (a, b) => true;
if(!variables) {
if (!variables) {
variables = [];
if(!variable_direction || variable_direction == 0) {
for(let field in json)
if (!variable_direction || variable_direction == 0) {
for (let field in json)
variables.push(field);
} else if(variable_direction == 1) {
for(let field in object)
} else if (variable_direction == 1) {
for (let field in object)
variables.push(field);
}
} else if(!Array.isArray(variables)) {
} else if (!Array.isArray(variables)) {
variables = [variables];
}
for(let field of variables) {
if(!json[field]) {
for (let field of variables) {
if (!json[field]) {
console.trace("Json does not contains %s", field);
continue;
}
if(!validator(field, json[field])) {
if (!validator(field, json[field])) {
console.trace("Validator results in false for %s", field);
continue;
}
@ -108,7 +108,9 @@ if(typeof ($) !== "undefined") {
}
if(!$.prototype.renderTag) {
$.prototype.renderTag = function (values?: any) : JQuery {
return $(this.render(values));
let result = $(this.render(values));
result.find("node").each((index, element) => { $(element).replaceWith(values[$(element).attr("key")]); });
return result;
}
}
}
@ -182,4 +184,12 @@ interface Window {
readonly OfflineAudioContext: typeof OfflineAudioContext;
readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext;
readonly RTCPeerConnection: typeof RTCPeerConnection;
readonly Pointer_stringify: any;
}
interface Navigator {
browserSpecs: {
name: string,
version: string
};
}

View File

@ -341,7 +341,7 @@ class ChannelEntry {
flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1);
}
spawnMenu(x, y, {
spawn_context_menu(x, y, {
type: MenuEntryType.ENTRY,
icon: "client-channel_switch",
name: "<b>Switch to channel</b>",

View File

@ -1,5 +1,6 @@
/// <reference path="channel.ts" />
/// <reference path="modal/ModalChangeVolume.ts" />
/// <reference path="modal/ModalServerGroupDialog.ts" />
enum ClientType {
CLIENT_VOICE,
@ -106,10 +107,113 @@ class ClientEntry {
}
}
protected assignment_context() : ContextMenuEntry[] {
let server_groups: ContextMenuEntry[] = [];
for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) {
if(group.type == GroupType.NORMAL) continue;
let entry: ContextMenuEntry = {} as any;
{
let tag = $.spawn("label").addClass("checkbox");
$.spawn("input").attr("type", "checkbox").prop("checked", this.groupAssigned(group)).appendTo(tag);
$.spawn("span").addClass("checkmark").appendTo(tag);
entry.icon = tag;
}
entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]";
if(this.groupAssigned(group)) {
entry.callback = () => {
this.channelTree.client.serverConnection.sendCommand("servergroupdelclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
});
};
entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
} else {
entry.callback = () => {
this.channelTree.client.serverConnection.sendCommand("servergroupaddclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
});
};
entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberAddPower);
}
entry.type = MenuEntryType.ENTRY;
server_groups.push(entry);
}
let channel_groups: ContextMenuEntry[] = [];
for(let group of this.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) {
if(group.type != GroupType.NORMAL) continue;
let entry: ContextMenuEntry = {} as any;
{
let tag = $.spawn("label").addClass("checkbox");
$.spawn("input").attr("type", "checkbox").prop("checked", this.assignedChannelGroup() == group.id).appendTo(tag);
$.spawn("span").addClass("checkmark").appendTo(tag);
entry.icon = tag;
}
entry.name = group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]";
entry.callback = () => {
this.channelTree.client.serverConnection.sendCommand("setclientchannelgroup", {
cldbid: this.properties.client_database_id,
cgid: group.id,
cid: this.currentChannel().channelId
});
};
entry.disabled = !this.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
entry.type = MenuEntryType.ENTRY;
channel_groups.push(entry);
}
return [{
type: MenuEntryType.SUB_MENU,
icon: "client-permission_server_groups",
name: "Set server group",
sub_menu: [
{
type: MenuEntryType.ENTRY,
icon: "client-permission_server_groups",
name: "Server groups dialog",
callback: () => {
Modals.createServerGroupAssignmentModal(this, (group, flag) => {
if(flag) {
return this.channelTree.client.serverConnection.sendCommand("servergroupaddclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
}).then(result => true);
} else
return this.channelTree.client.serverConnection.sendCommand("servergroupdelclient", {
sgid: group.id,
cldbid: this.properties.client_database_id
}).then(result => true);
});
}
},
MenuEntry.HR(),
...server_groups
]
},{
type: MenuEntryType.SUB_MENU,
icon: "client-permission_channel",
name: "Set channel group",
sub_menu: [
...channel_groups
]
},{
type: MenuEntryType.SUB_MENU,
icon: "client-permission_client",
name: "Permissions",
disabled: true,
sub_menu: [ ]
}];
}
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
const _this = this;
spawnMenu(x, y,
spawn_context_menu(x, y,
{
type: MenuEntryType.ENTRY,
icon: "client-change_nickname",
@ -151,6 +255,8 @@ class ClientEntry {
}, { width: 400, maxLength: 1024 }).open();
}
},
MenuEntry.HR(),
...this.assignment_context(),
MenuEntry.HR(), {
type: MenuEntryType.ENTRY,
icon: "client-move_client_to_own_channel",
@ -524,7 +630,7 @@ class LocalClientEntry extends ClientEntry {
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
const _self = this;
spawnMenu(x, y,
spawn_context_menu(x, y,
{
name: "<b>Change name</b>",
icon: "client-change_nickname",
@ -640,7 +746,7 @@ class MusicClientEntry extends ClientEntry {
}
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
spawnMenu(x, y,
spawn_context_menu(x, y,
{
name: "<b>Change bot name</b>",
icon: "client-change_nickname",
@ -661,6 +767,8 @@ class MusicClientEntry extends ClientEntry {
type: MenuEntryType.ENTRY
},
MenuEntry.HR(),
...super.assignment_context(),
MenuEntry.HR(),
{
name: "Delete bot",
icon: "client-delete",

View File

@ -35,7 +35,7 @@ class ControlBar {
this.htmlTag.find(".btn_mute_input").on('click', this.onInputMute.bind(this));
this.htmlTag.find(".btn_mute_output").on('click', this.onOutputMute.bind(this));
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
{
let tokens = this.htmlTag.find(".btn_token");
tokens.find(".button-dropdown").on('click', () => {
@ -247,4 +247,13 @@ class ControlBar {
private on_token_list() {
createErrorModal("Not implemented", "Token list is not implemented yet!").open();
}
private onPermission() {
let button = this.htmlTag.find(".btn_permissions");
button.addClass("activated");
setTimeout(() => {
Modals.spawnPermissionEdit().open();
button.removeClass("activated");
}, 0);
}
}

View File

@ -117,7 +117,6 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
let properties = this.buildProperties(client);
let rendered = $("#tmpl_selected_client").renderTag([properties]);
rendered.find("node").each((index, element) => { $(element).replaceWith(properties[$(element).attr("key")]); });
html_tag.append(rendered);
this.registerInterval(setInterval(() => {
@ -190,7 +189,6 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
properties["property_" + key] = server.properties[key];
let rendered = $("#tmpl_selected_server").renderTag([properties]);
rendered.find("node").each((index, element) => { $(element).replaceWith(properties[$(element).attr("key")]); });
this.registerInterval(setInterval(() => {
html_tag.find(".update_onlinetime").text(formatDate(server.calculateUptime()));
@ -255,7 +253,6 @@ class ChannelInfoManager extends InfoManager<ChannelEntry> {
});
let rendered = $("#tmpl_selected_channel").renderTag([properties]);
rendered.find("node").each((index, element) => { $(element).replaceWith(properties[$(element).attr("key")]); });
html_tag.append(rendered);
}
@ -478,7 +475,6 @@ class MusicInfoManager extends ClientInfoManager {
}
let rendered = $("#tmpl_selected_music").renderTag([properties]);
rendered.find("node").each((index, element) => { $(element).replaceWith(properties[$(element).attr("key")]); });
html_tag.append(rendered);
}

View File

@ -0,0 +1,874 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
/// <reference path="../../client.ts" />
namespace Modals {
export function spawnPermissionEdit() : Modal {
const connectModal = createModal({
header: function() {
return "Server Permissions";
},
body: function () {
let properties: any = {};
let start, end;
start = Date.now();
{
let groups = globalClient.permissions.groupedPermissions();
let root_entry: any = {};
root_entry.entries = [];
let entry_stack: any[] = [root_entry];
let insert_group = (group: GroupedPermissions) => {
let group_entry: any = {};
group_entry.type = "group";
group_entry.name = group.group.name;
group_entry.entries = [];
entry_stack.last().entries.push(group_entry);
entry_stack.push(group_entry);
for(let child of group.children)
insert_group(child);
entry_stack.pop();
for(let perm of group.permissions) {
let entry: any = {};
entry.type = "entry";
entry.permission_name = perm.name;
entry.unset = true;
group_entry.entries.push(entry);
{
let tag: JQuery<HTMLElement>;
if(perm.name.startsWith("b_")) {
tag = $.spawn("label").addClass("checkbox");
$.spawn("input").attr("type", "checkbox").appendTo(tag);
$.spawn("span").addClass("checkmark").appendTo(tag);
} else {
tag = $.spawn("input");
tag.attr("type", "number");
tag.attr("min-value", -1);
tag.attr("max-value", 9999999999); //TODO use there may the grant permission
}
root_entry[perm.name + "_value"] = tag;
}
{
let tag = $.spawn("label").addClass("checkbox");
$.spawn("input").attr("type", "checkbox").appendTo(tag);
$.spawn("span").addClass("checkmark").appendTo(tag);
root_entry[perm.name + "_skip"] = tag;
}
{
let tag = $.spawn("label").addClass("checkbox");
$.spawn("input").attr("type", "checkbox").appendTo(tag);
$.spawn("span").addClass("checkmark").appendTo(tag);
root_entry[perm.name + "_negate"] = tag;
}
{
let tag = $.spawn("input");
tag.attr("type", "number");
tag.attr("min-value", -1);
tag.attr("max-value", 9999999999);
root_entry[perm.name + "_grant"] = tag;
}
//{{>permission_name}}_value
}
};
groups.forEach(entry => insert_group(entry));
root_entry.permissions = root_entry.entries;
properties["permissions_group_server"] = $("#tmpl_permission_explorer").renderTag(root_entry);
properties["permissions_group_channel"] = properties["permissions_group_server"].clone();
properties["permissions_channel"] = properties["permissions_group_server"].clone();
properties["permissions_client"] = properties["permissions_group_server"].clone();
properties["permissions_client_channel"] = properties["permissions_group_server"].clone();
}
end = Date.now();
console.log("Generate: %s", end - start);
start = end;
let tag = $.spawn("div").append($("#tmpl_server_permissions").renderTag(properties)).tabify(false);
end = Date.now();
console.log("Tab: %s", end - start);
start = end;
apply_server_groups(tag.find(".layout-group-server"));
apply_channel_groups(tag.find(".layout-group-channel"));
apply_channel_permission(tag.find(".layout-channel"));
apply_client_permission(tag.find(".layout-client"));
apply_client_channel_permission(tag.find(".layout-client-channel"));
end = Date.now();
console.log("Listeners: %s", end - start);
start = end;
return tag;
},
footer: function () {
let tag = $.spawn("div");
tag.css("text-align", "right");
tag.css("margin-top", "3px");
tag.css("margin-bottom", "6px");
tag.addClass("modal-button-group");
let buttonOk = $.spawn("button");
buttonOk.text("Close").addClass("btn_close");
tag.append(buttonOk);
return tag;
},
width: "90%"
});
connectModal.htmlTag.find(".btn_close").on('click', () => {
connectModal.close();
});
return connectModal;
}
function display_permissions(permission_tag: JQuery, permissions: PermissionValue[]) {
permission_tag.find(".permission").addClass("unset").find(".permission-grant input").val("");
for(let perm of permissions) {
let tag = permission_tag.find("." + perm.type.name);
if(perm.value != undefined) {
tag.removeClass("unset");
{
let value = tag.find(".permission-value input");
if(value.attr("type") == "checkbox")
value.prop("checked", perm.value == 1);
else
value.val(perm.value);
}
tag.find(".permission-skip input").prop("checked", perm.flag_skip);
tag.find(".permission-negate input").prop("checked", perm.flag_negate);
}
if(perm.granted_value != undefined) {
tag.find(".permission-grant input").val(perm.granted_value);
}
}
permission_tag.find(".filter-input").trigger('change');
}
function make_permission_editor(tag: JQuery, default_number: number, cb_edit: (type: PermissionInfo, value?: number, skip?: boolean, negate?: boolean) => Promise<boolean>, cb_grant_edit: (type: PermissionInfo, value?: number) => Promise<boolean>) {
tag = tag.hasClass("permission-explorer") ? tag : tag.find(".permission-explorer");
const list = tag.find(".list");
list.css("max-height", document.body.clientHeight * .7)
list.find(".arrow").each((idx, _entry) => {
let entry = $(_entry);
let entries = entry.parentsUntil(".group").first().parent().find("> .group-entries");
entry.on('click', () => {
if(entry.hasClass("right")) {
entries.show();
} else {
entries.hide();
}
entry.toggleClass("right down");
});
});
tag.find(".filter-input, .filter-granted").on('keyup change', event => {
let filter_mask = tag.find(".filter-input").val() as string;
let req_granted = tag.find('.filter-granted').prop("checked");
tag.find(".permission").each((idx, _entry) => {
let entry = $(_entry);
let key = entry.find("> .filter-key");
let should_hide = filter_mask.length != 0 && key.text().indexOf(filter_mask) == -1;
if(req_granted) {
if(entry.hasClass("unset") && entry.find(".permission-grant input").val() == "")
should_hide = true;
}
entry.attr("match", should_hide ? 0 : 1);
if(should_hide)
entry.hide();
else
entry.show();
});
tag.find(".group").each((idx, _entry) => {
let entry = $(_entry);
let target = entry.find(".entry:not(.group)[match=\"1\"]").length > 0;
if(target)
entry.show();
else
entry.hide();
});
});
const expend_all = (parent) => {
(parent || list).find(".arrow").addClass("right").removeClass("down").trigger('click');
};
const collapse_all = (parent) => {
(parent || list).find(".arrow").removeClass("right").addClass("down").trigger('click');
};
list.on('contextmenu', event => {
if (event.isDefaultPrevented()) return;
event.preventDefault();
spawn_context_menu(event.pageX, event.pageY, {
type: MenuEntryType.ENTRY,
icon: "",
name: "Expend all",
callback: () => expend_all.bind(this, [undefined])
},{
type: MenuEntryType.ENTRY,
icon: "",
name: "Collapse all",
callback: collapse_all.bind(this, [undefined])
});
});
tag.find(".title").each((idx, _entry) => {
let entry = $(_entry);
entry.on('click', () => {
tag.find(".selected").removeClass("selected");
$(_entry).addClass("selected");
});
entry.on('contextmenu', event => {
if (event.isDefaultPrevented()) return;
event.preventDefault();
spawn_context_menu(event.pageX, event.pageY, {
type: MenuEntryType.ENTRY,
icon: "",
name: "Expend group",
callback: () => expend_all.bind(this, entry)
}, {
type: MenuEntryType.ENTRY,
icon: "",
name: "Expend all",
callback: () => expend_all.bind(this, undefined)
}, {
type: MenuEntryType.ENTRY,
icon: "",
name: "Collapse group",
callback: collapse_all.bind(this, entry)
}, {
type: MenuEntryType.ENTRY,
icon: "",
name: "Collapse all",
callback: () => expend_all.bind(this, undefined)
});
});
});
tag.find(".permission").each((idx, _entry) => {
let entry = $(_entry);
entry.on('click', () => {
tag.find(".selected").removeClass("selected");
$(_entry).addClass("selected");
});
entry.on('dblclick', event => {
entry.removeClass("unset");
let value = entry.find("> .permission-value input");
if(value.attr("type") == "number")
value.focus().val(default_number).trigger('change');
else
value.prop("checked", true).trigger('change');
});
entry.on('contextmenu', event => {
if(event.isDefaultPrevented()) return;
event.preventDefault();
let entries: ContextMenuEntry[] = [];
if(entry.hasClass("unset")) {
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Add permission",
callback: () => entry.trigger('dblclick')
});
} else {
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Remove permission",
callback: () => {
entry.addClass("unset");
entry.find(".permission-value input").val("").trigger('change');
}
});
}
if(entry.find("> .permission-grant input").val() == "") {
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Add grant permission",
callback: () => {
let value = entry.find("> .permission-grant input");
value.focus().val(default_number).trigger('change');
}
});
} else {
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Remove permission",
callback: () => {
entry.find("> .permission-grant input").val("").trigger('change');
}
});
}
entries.push(MenuEntry.HR());
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Expend all",
callback: () => expend_all.bind(this, undefined)
});
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Collapse all",
callback: collapse_all.bind(this, undefined)
});
entries.push(MenuEntry.HR());
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Show permission description",
callback: () => {
createErrorModal("Not implemented!", "This function isnt implemented yet!").open();
}
});
entries.push({
type: MenuEntryType.ENTRY,
icon: "",
name: "Copy permission name",
callback: () => {
copy_to_clipboard(entry.find(".permission-name").text() as string);
}
});
spawn_context_menu(event.pageX, event.pageY, ...entries);
});
entry.find(".permission-value input, .permission-negate input, .permission-skip input").on('change', event => {
let permission = globalClient.permissions.resolveInfo(entry.find(".permission-name").text());
if(!permission) {
console.error("Attempted to edit a not known permission! (%s)", entry.find(".permission-name").text());
return;
}
if(entry.hasClass("unset")) {
cb_edit(permission, undefined, undefined, undefined).catch(error => {
tag.find(".button-update").trigger('click');
});
} else {
let input = entry.find(".permission-value input");
let value = input.attr("type") == "number" ? input.val() : (input.prop("checked") ? "1" : "0");
if(value == "" || isNaN(value as number)) value = 0;
else value = parseInt(value as string);
let negate = entry.find(".permission-negate input").prop("checked");
let skip = entry.find(".permission-skip input").prop("checked");
cb_edit(permission, value, skip, negate).catch(error => {
tag.find(".button-update").trigger('click');
});
}
});
entry.find(".permission-grant input").on('change', event => {
let permission = globalClient.permissions.resolveInfo(entry.find(".permission-name").text());
if(!permission) {
console.error("Attempted to edit a not known permission! (%s)", entry.find(".permission-name").text());
return;
}
let value = entry.find(".permission-grant input").val();
if(value && value != "" && !isNaN(value as number)) {
cb_grant_edit(permission, parseInt(value as string)).catch(error => {
tag.find(".button-update").trigger('click');
});
} else cb_grant_edit(permission, undefined).catch(error => {
tag.find(".button-update").trigger('click');
});
});
});
}
function build_channel_tree(channel_list: JQuery, update_button: JQuery) {
for(let channel of globalClient.channelTree.channels) {
let tag = $.spawn("div").addClass("channel").attr("channel-id", channel.channelId);
globalClient.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(tag);
{
let name = $.spawn("a").text(channel.channelName() + " (" + channel.channelId + ")").addClass("name");
//if(globalClient.channelTree.server.properties. == group.id)
// name.addClass("default");
name.appendTo(tag);
}
tag.appendTo(channel_list);
tag.on('click', event => {
channel_list.find(".selected").removeClass("selected");
tag.addClass("selected");
update_button.trigger('click');
});
}
setTimeout(() => channel_list.find('.channel').first().trigger('click'), 0);
}
function apply_client_channel_permission(tag: JQuery) {
let permission_tag = tag.find(".permission-explorer");
let channel_list = tag.find(".list-channel .entries");
permission_tag.addClass("disabled");
make_permission_editor(permission_tag, 75, (type, value, skip, negate) => {
let cldbid = parseInt(tag.find(".client-dbid").val() as string);
if(isNaN(cldbid)) return Promise.reject("invalid cldbid");
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return Promise.reject("invalid channel");
}
if(value != undefined) {
console.log("Added permission " + type.name + " with properties: %o %o %o", value, skip, negate);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channelclientaddperm", {
cldbid: cldbid,
cid: channel.channelId,
permid: type.id,
permvalue: value,
permskip: skip,
permnegate: negate
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed permission " + type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channelclientdelperm", {
cldbid: cldbid,
cid: channel.channelId,
permid: type.id
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
}, (type, value) => {
let cldbid = parseInt(tag.find(".client-dbid").val() as string);
if(isNaN(cldbid)) return Promise.reject("invalid cldbid");
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return Promise.reject("invalid channel");
}
if(value != undefined) {
console.log("Added grant of %o for " + type.name, value);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channelclientaddperm", {
cldbid: cldbid,
cid: channel.channelId,
permid: type.id | (1 << 15),
permvalue: value,
permskip: false,
permnegate: false
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed grant permission for %s", type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channelclientdelperm", {
cldbid: cldbid,
cid: channel.channelId,
permid: type.id | (1 << 15)
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
});
build_channel_tree(channel_list, permission_tag.find(".button-update"));
permission_tag.find(".button-update").on('click', event => {
let val = tag.find('.client-select-uid').val();
globalClient.serverConnection.helper.info_from_uid(val as string).then(result => {
if(!result || result.length == 0) return Promise.reject("invalid data");
permission_tag.removeClass("disabled");
tag.find(".client-name").val(result[0].client_nickname);
tag.find(".client-uid").val(result[0].client_unique_id);
tag.find(".client-dbid").val(result[0].client_database_id);
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return Promise.reject();
}
return globalClient.permissions.requestClientChannelPermissions(channel.channelId, result[0].client_database_id).then(result => display_permissions(permission_tag, result));
}).catch(error => {
console.log(error); //TODO error handling?
permission_tag.addClass("disabled");
});
});
tag.find(".client-select-uid").on('change', event => {
tag.find(".button-update").trigger('click');
});
}
function apply_client_permission(tag: JQuery) {
let permission_tag = tag.find(".permission-explorer");
permission_tag.addClass("disabled");
make_permission_editor(permission_tag, 75, (type, value, skip, negate) => {
let cldbid = parseInt(tag.find(".client-dbid").val() as string);
if(isNaN(cldbid)) return Promise.reject("invalid cldbid");
if(value != undefined) {
console.log("Added permission " + type.name + " with properties: %o %o %o", value, skip, negate);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("clientaddperm", {
cldbid: cldbid,
permid: type.id,
permvalue: value,
permskip: skip,
permnegate: negate
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed permission " + type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("clientdelperm", {
cldbid: cldbid,
permid: type.id
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
}, (type, value) => {
let cldbid = parseInt(tag.find(".client-dbid").val() as string);
if(isNaN(cldbid)) return Promise.reject("invalid cldbid");
if(value != undefined) {
console.log("Added grant of %o for " + type.name, value);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("clientaddperm", {
cldbid: cldbid,
permid: type.id | (1 << 15),
permvalue: value,
permskip: false,
permnegate: false
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed grant permission for %s", type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("clientdelperm", {
cldbid: cldbid,
permid: type.id | (1 << 15)
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
});
tag.find(".client-select-uid").on('change', event => {
tag.find(".button-update").trigger('click');
});
permission_tag.find(".button-update").on('click', event => {
let val = tag.find('.client-select-uid').val();
globalClient.serverConnection.helper.info_from_uid(val as string).then(result => {
if(!result || result.length == 0) return Promise.reject("invalid data");
permission_tag.removeClass("disabled");
tag.find(".client-name").val(result[0].client_nickname);
tag.find(".client-uid").val(result[0].client_unique_id);
tag.find(".client-dbid").val(result[0].client_database_id);
return globalClient.permissions.requestClientPermissions(result[0].client_database_id).then(result => display_permissions(permission_tag, result));
}).catch(error => {
console.log(error); //TODO error handling?
permission_tag.addClass("disabled");
});
});
}
function apply_channel_permission(tag: JQuery) {
let channel_list = tag.find(".list-channel .entries");
let permission_tag = tag.find(".permission-explorer");
make_permission_editor(tag, 75, (type, value, skip, negate) => {
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added permission " + type.name + " with properties: %o %o %o", value, skip, negate);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channeladdperm", {
cid: channel.channelId,
permid: type.id,
permvalue: value,
permskip: skip,
permnegate: negate
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed permission " + type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channeldelperm", {
cid: channel.channelId,
permid: type.id
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
}, (type, value) => {
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added grant of %o for " + type.name, value);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channeladdperm", {
cid: channel.channelId,
permid: type.id | (1 << 15),
permvalue: value,
permskip: false,
permnegate: false
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed grant permission for %s", type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channeldelperm", {
cid: channel.channelId,
permid: type.id | (1 << 15)
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
});
build_channel_tree(channel_list, permission_tag.find(".button-update"));
permission_tag.find(".button-update").on('click', event => {
let channel_id: number = parseInt(channel_list.find(".selected").attr("channel-id"));
let channel = globalClient.channelTree.findChannel(channel_id);
if(!channel) {
console.warn("Missing selected channel id for permission editor action!");
return;
}
globalClient.permissions.requestChannelPermissions(channel.channelId).then(result => display_permissions(permission_tag, result)).catch(error => {
console.log(error); //TODO handling?
});
});
}
function apply_channel_groups(tag: JQuery) {
let group_list = tag.find(".list-group-channel .entries");
let permission_tag = tag.find(".permission-explorer");
make_permission_editor(tag, 75, (type, value, skip, negate) => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.channelGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added permission " + type.name + " with properties: %o %o %o", value, skip, negate);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channelgroupaddperm", {
cgid: group.id,
permid: type.id,
permvalue: value,
permskip: skip,
permnegate: negate
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed permission " + type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channelgroupdelperm", {
cgid: group.id,
permid: type.id
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
}, (type, value) => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.channelGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added grant of %o for " + type.name, value);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("channelgroupaddperm", {
cgid: group.id,
permid: type.id | (1 << 15),
permvalue: value,
permskip: false,
permnegate: false
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
else {
console.log("Removed grant permission for %s", type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("channelgroupdelperm", {
cgid: group.id,
permid: type.id | (1 << 15)
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
});
{
for(let group of globalClient.groups.channelGroups.sort(GroupManager.sorter())) {
let tag = $.spawn("div").addClass("group").attr("group-id", group.id);
globalClient.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
{
let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name");
if(group.properties.savedb)
name.addClass("savedb");
if(globalClient.channelTree.server.properties.virtualserver_default_channel_group == group.id)
name.addClass("default");
name.appendTo(tag);
}
tag.appendTo(group_list);
tag.on('click', event => {
group_list.find(".selected").removeClass("selected");
tag.addClass("selected");
permission_tag.find(".button-update").trigger('click');
});
}
}
//button-update
permission_tag.find(".button-update").on('click', event => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.channelGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor!");
return;
}
globalClient.groups.request_permissions(group).then(result => display_permissions(permission_tag, result)).catch(error => {
console.log(error); //TODO handling?
});
});
setTimeout(() => group_list.find('.group').first().trigger('click'), 0);
}
function apply_server_groups(tag: JQuery) {
let group_list = tag.find(".list-group-server .entries");
let permission_tag = tag.find(".permission-explorer");
make_permission_editor(tag, 75, (type, value, skip, negate) => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.serverGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added permission " + type.name + " with properties: %o %o %o", value, skip, negate);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("servergroupaddperm", {
sgid: group.id,
permid: type.id,
permvalue: value,
permskip: skip,
permnegate: negate
}).then(resolve.bind(undefined, true)).catch(reject);
});
} else {
console.log("Removed permission " + type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("servergroupdelperm", {
sgid: group.id,
permid: type.id
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
}, (type, value) => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.serverGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor action!");
return;
}
if(value != undefined) {
console.log("Added grant of %o for " + type.name, value);
return new Promise<boolean>((resolve, reject) => {
globalClient.serverConnection.sendCommand("servergroupaddperm", {
sgid: group.id,
permid: type.id | (1 << 15),
permvalue: value,
permskip: false,
permnegate: false
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
else {
console.log("Removed grant permission for %s", type.name);
return new Promise<boolean>((resolve, reject) => {
return globalClient.serverConnection.sendCommand("servergroupdelperm", {
sgid: group.id,
permid: type.id | (1 << 15)
}).then(resolve.bind(undefined, true)).catch(reject);
});
}
});
{
for(let group of globalClient.groups.serverGroups.sort(GroupManager.sorter())) {
let tag = $.spawn("div").addClass("group").attr("group-id", group.id);
globalClient.fileManager.icons.generateTag(group.properties.iconid).appendTo(tag);
{
let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name");
if(group.properties.savedb)
name.addClass("savedb");
if(globalClient.channelTree.server.properties.virtualserver_default_server_group == group.id)
name.addClass("default");
name.appendTo(tag);
}
tag.appendTo(group_list);
tag.on('click', event => {
group_list.find(".selected").removeClass("selected");
tag.addClass("selected");
permission_tag.find(".button-update").trigger('click');
});
}
}
//button-update
permission_tag.find(".button-update").on('click', event => {
let group_id: number = parseInt(group_list.find(".selected").attr("group-id"));
let group = globalClient.groups.serverGroup(group_id);
if(!group) {
console.warn("Missing selected group id for permission editor!");
return;
}
globalClient.groups.request_permissions(group).then(result => display_permissions(permission_tag, result)).catch(error => {
console.log(error); //TODO handling?
});
});
setTimeout(() => group_list.find('.group').first().trigger('click'), 0);
}
}

View File

@ -0,0 +1,64 @@
namespace Modals {
export function createServerGroupAssignmentModal(client: ClientEntry, callback: (group: Group, flag: boolean) => Promise<boolean>) {
const modal = createModal({
header: "Server Groups",
body: () => {
let tag: any = {};
let groups = tag["groups"] = [];
tag["client_name"] = client.clientNickName();
for(let group of client.channelTree.client.groups.serverGroups) {
if(group.type != GroupType.NORMAL) continue;
let entry = {} as any;
entry["id"] = group.id;
entry["name"] = group.name;
entry["assigned"] = client.groupAssigned(group);
entry["disabled"] = !client.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower);
tag["icon_" + group.id] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid);
groups.push(entry);
}
let template = $("#tmpl_server_group_assignment").renderTag(tag);
template.find(".group-entry input").each((_idx, _entry) => {
let entry = $(_entry);
entry.on('change', event => {
let group_id = parseInt(entry.attr("group-id"));
let group = client.channelTree.client.groups.serverGroup(group_id);
if(!group) {
console.warn("Could not resolve target group!");
return false;
}
let target = entry.prop("checked");
callback(group, target).then(flag => flag ? Promise.resolve() : Promise.reject()).catch(error => entry.prop("checked", !target));
});
});
return template;
},
footer: () => {
let footer = $.spawn("div");
footer.addClass("modal-button-group");
footer.css("margin", "5px");
let button_close = $.spawn("button");
button_close.text("Close").addClass("button_close");
footer.append(button_close);
return footer;
},
width: "max-content"
});
modal.htmlTag.find(".button_close").click(() => {
modal.close();
});
modal.open();
}
}

View File

@ -121,7 +121,7 @@ class ServerEntry {
}
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
spawnMenu(x, y, {
spawn_context_menu(x, y, {
type: MenuEntryType.ENTRY,
icon: "client-virtualserver_edit",
name: "Edit",

View File

@ -36,7 +36,7 @@ class ChannelTree {
this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT).granted(1) ||
this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_PERMANENT).granted(1);
spawnMenu(x, y,
spawn_context_menu(x, y,
{
type: MenuEntryType.ENTRY,
icon: "client-channel_create",

View File

@ -8,4 +8,74 @@ namespace helpers {
});
});
}
}
}
class LaterPromise<T> extends Promise<T> {
private _handle: Promise<T>;
private _resolve: ($: T) => any;
private _reject: ($: any) => any;
private _time: number;
constructor() {
super((resolve, reject) => {});
this._handle = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
this._time = Date.now();
}
resolved(object: T) {
this._resolve(object);
}
rejected(reason) {
this._reject(reason);
}
function_rejected() {
return error => this.rejected(error);
}
time() { return this._time; }
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
return this._handle.then(onfulfilled, onrejected);
}
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult> {
return this._handle.then(onrejected);
}
}
const copy_to_clipboard = str => {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
const selected =
document.getSelection().rangeCount > 0
? document.getSelection().getRangeAt(0)
: false;
el.select();
document.execCommand('copy');
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
};

View File

@ -1,5 +1,5 @@
$(document).on("mousedown",function (e) {
if($(e.target).parents(".modal").length == 0){
if($(e.target).parents("#contextMenu").length == 0 && $(e.target).parents(".modal").length == 0){
$(".modal:visible").last().find(".close").trigger("click");
}
});

View File

@ -1,7 +1,7 @@
interface JQuery {
asTabWidget() : JQuery;
tabify() : this;
asTabWidget(copy?: boolean) : JQuery;
tabify(copy?: boolean) : this;
changeElementType(type: string) : JQuery;
}
@ -21,8 +21,8 @@ if(typeof (customElements) !== "undefined") {
}
var TabFunctions = {
tabify(template: JQuery) : JQuery {
console.log("Tabify:");
tabify(template: JQuery, copy: boolean = true) : JQuery {
console.log("Tabify: copy=" + copy);
console.log(template);
let tag = $.spawn("div");
@ -40,10 +40,13 @@ var TabFunctions = {
template.find("x-entry").each(function () {
let hentry = $.spawn("div");
hentry.addClass("entry");
hentry.append($(this).find("x-tag").clone(true, true));
if(copy)
hentry.append($(this).find("x-tag").clone(true, true));
else
hentry.append($(this).find("x-tag"));
const _this = $(this);
const _entryContent = _this.find("x-content").clone(true, true);
const _entryContent = copy ? _this.find("x-content").clone(true, true) : _this.find("x-content");
silentContent.append(_entryContent);
hentry.on("click", function () {
if(hentry.hasClass("selected")) return;
@ -72,9 +75,9 @@ var TabFunctions = {
}
if(!$.fn.asTabWidget) {
$.fn.asTabWidget = function () : JQuery {
$.fn.asTabWidget = function (copy?: boolean) : JQuery {
if($(this).prop("tagName") == "X-TAB")
return TabFunctions.tabify($(this));
return TabFunctions.tabify($(this), typeof(copy) === "boolean" ? copy : true);
else {
throw "Invalid tag! " + $(this).prop("tagName");
}
@ -82,13 +85,13 @@ if(!$.fn.asTabWidget) {
}
if(!$.fn.tabify) {
$.fn.tabify = function () {
$.fn.tabify = function (copy?: boolean) {
try {
let self = this.asTabWidget();
let self = this.asTabWidget(copy);
this.replaceWith(self);
} catch(object) {}
this.find("x-tab").each(function () {
$(this).replaceWith($(this).asTabWidget());
$(this).replaceWith($(this).asTabWidget(copy));
});
return this;
}

View File

@ -57,7 +57,7 @@ class AudioController {
}
static initialized() : boolean {
return this.globalContext.state === "running";
return (this.globalContext || {state: ""}).state === "running";
}
static on_initialized(callback: () => any) {

View File

@ -13,6 +13,61 @@
<link rel="stylesheet" href="css/general.css" type="text/css">
</head>
<body>
<!-- Template for the connect modal -->
<script id="tmpl_connect" type="text/html">
<div style="margin-top: 5px;">
<div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between">
<div style="width: 68%; margin-bottom: 5px">
<div>Remote address and port:</div>
<input type="text" style="width: 100%" class="connect_address" value="unknown">
</div>
<div style="width: 20%">
<div>Server password:</div>
<form name="server_password_form" onsubmit="return false;">
<input type="password" if="connect_server_password_{{rnd '0~13377331'/}}" autocomplete="off" style="width: 100%" class="connect_password">
</form>
</div>
</div>
<div>
<div>Nickname:</div>
<input type="text" style="width: 100%" class="connect_nickname" value="">
</div>
<hr>
<div class="group_box">
<div style="display: flex; justify-content: space-between;">
<div style="text-align: right;">Identity Settings</div>
<select class="identity_select">
<option name="identity_type" value="TEAFORO">Forum Account</option>
<option name="identity_type" value="TEAMSPEAK">TeamSpeak</option>
<option name="identity_type" value="NICKNAME">Nickname (Debug purposes only!)</option> <!-- Only available on localhost for debug -->
</select>
</div>
<hr>
<div class="identity_config_TEAMSPEAK identity_config">
Please enter your exported TS3 Identity string bellow or select your exported Identity<br>
<div style="width: 100%; display: flex; flex-direction: row">
<input placeholder="Identity string" style="width: 70%; margin: 5px;" class="identity_string">
<div style="width: 30%; margin: 5px"><input class="identity_file" type="file"></div>
</div>
</div>
<div class="identity_config_TEAFORO identity_config">
{{if forum_valid}}
You're using your forum account as verification
{{else}}
You cant use your TeaSpeak forum account.<br>
You're not connected!<br>
Click <a href="{{:forum_path}}login.php">here</a> to login via the TeaSpeak forum.
{{/if}}
</div>
<div class="identity_config_NICKNAME identity_config">
This is just for debug and uses the name as unique identifier
</div>
<div style="background-color: red; border-radius: 1px; display: none" class="error_message"></div>
</div> <!-- <a href="<?php echo authPath() . "login.php"; ?>">Login</a> via the TeaSpeak forum. -->
</div>
</script>
<!-- Template for chennel create & edit-->
<script id="tmpl_channel_edit" type="text/html">
<div class="align_column general_properties">
@ -182,7 +237,6 @@
</x-entry>
</x-tab>
</script>
<script id="tmpl_server_edit" type="text/html">
<div class="align_column properties_general server_properties">
<div class="properties">
@ -414,62 +468,6 @@
</x-tab>
</script>
<!-- Template for the connect modal -->
<script id="tmpl_connect" type="text/html">
<div style="margin-top: 5px;">
<div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between">
<div style="width: 68%; margin-bottom: 5px">
<div>Remote address and port:</div>
<input type="text" style="width: 100%" class="connect_address" value="unknown">
</div>
<div style="width: 20%">
<div>Server password:</div>
<form name="server_password_form" onsubmit="return false;">
<input type="password" if="connect_server_password_{{rnd '0~13377331'/}}" autocomplete="off" style="width: 100%" class="connect_password">
</form>
</div>
</div>
<div>
<div>Nickname:</div>
<input type="text" style="width: 100%" class="connect_nickname" value="">
</div>
<hr>
<div class="group_box">
<div style="display: flex; justify-content: space-between;">
<div style="text-align: right;">Identity Settings</div>
<select class="identity_select">
<option name="identity_type" value="TEAFORO">Forum Account</option>
<option name="identity_type" value="TEAMSPEAK">TeamSpeak</option>
<option name="identity_type" value="NICKNAME">Nickname (Debug purposes only!)</option> <!-- Only available on localhost for debug -->
</select>
</div>
<hr>
<div class="identity_config_TEAMSPEAK identity_config">
Please enter your exported TS3 Identity string bellow or select your exported Identity<br>
<div style="width: 100%; display: flex; flex-direction: row">
<input placeholder="Identity string" style="width: 70%; margin: 5px;" class="identity_string">
<div style="width: 30%; margin: 5px"><input class="identity_file" type="file"></div>
</div>
</div>
<div class="identity_config_TEAFORO identity_config">
{{if forum_valid}}
You're using your forum account as verification
{{else}}
You cant use your TeaSpeak forum account.<br>
You're not connected!<br>
Click <a href="{{:forum_path}}login.php">here</a> to login via the TeaSpeak forum.
{{/if}}
</div>
<div class="identity_config_NICKNAME identity_config">
This is just for debug and uses the name as unique identifier
</div>
<div style="background-color: red; border-radius: 1px; display: none" class="error_message"></div>
</div> <!-- <a href="<?php echo authPath() . "login.php"; ?>">Login</a> via the TeaSpeak forum. -->
</div>
</script>
<!-- Template for the settings -->
<script id="tmpl_settings" type="text/html">
<x-tab>
@ -524,14 +522,12 @@
</x-entry>
</x-tab>
</script>
<script id="tmpl_change_volume" type="text/html">
<div style="display: flex; justify-content: center; vertical-align: center">
<input type="range" min="0" max="200" value="100" class="volume_slider" style="width: 100%">
<div class="display_volume" style="width: 60px; align-self: center; text-align: center">&plusmn;0 %</div>
</div>
</script>
<script id="tmpl_client_ban" type="text/html">
<div class="align_column">
<div class="align_column" style="margin: 5px">
@ -558,6 +554,192 @@
</div>
</script>
<!-- Permission overview -->
<script id="tmpl_server_permissions" type="text/html">
<x-tab>
<x-entry>
<x-tag>Server Groups</x-tag>
<x-content>
<div class="layout-group-server">
<div class="list-group-server">
<div class="entries"></div>
</div>
<node key="permissions_group_server"/>
<div class="list-group-server-clients"></div>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>Channel Groups</x-tag>
<x-content>
<div class="layout-group-channel">
<div class="list-group-channel">
<div class="entries"></div>
</div>
<node key="permissions_group_channel"/>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>Channel permissions</x-tag>
<x-content>
<div class="layout-channel">
<div class="list-channel">
<div class="entries"></div>
</div>
<node key="permissions_channel"/>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>Client permissions</x-tag>
<x-content>
<div class="layout-client">
<div class="client-info">
<div class="client-select">
<a>Client unique ID:</a>
<input type="text" class="client-select-uid">
</div>
<hr>
<div class="client-info">
<a>Nickname:</a>
<input class="client-name">
<a>Unique ID:</a>
<input class="client-uid">
<a>Cleint database ID:</a>
<input class="client-dbid">
</div>
</div>
<node key="permissions_client"/>
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag>Client channel permissions</x-tag>
<x-content>
<div class="layout-client-channel">
<div class="client-info">
<div class="client-select">
<a>Client unique ID:</a>
<input type="text" class="client-select-uid">
</div>
<hr>
<div class="client-info">
<a>Nickname:</a>
<input class="client-name">
<a>Unique ID:</a>
<input class="client-uid">
<a>Cleint database ID:</a>
<input class="client-dbid">
</div>
<hr>
<div class="list-channel">
<div class="entries"></div>
</div>
</div>
<node key="permissions_client_channel"/>
</div>
</x-content>
</x-entry>
</x-tab>
</script>
<script id="tmpl_server_group_assignment" type="text/html">
<div class="group-assignment-list">
<a>Changing groups of <b>{{>client_name}}</b></a>
<div class="group-list">
{{for groups}}
<div class="group-entry">
<label class="checkbox {{if disabled}}disabled{{/if}}">
<input type="checkbox" group-id="{{>id}}" {{if assigned}}checked{{/if}}>
<span class="checkmark"></span>
</label>
<node key="icon_{{>id}}"></node>
<a>{{>name}} ({{>id}})</a>
</div>
{{/for}}
</div>
</div>
</script>
<script id="tmpl_permission_explorer" type="text/html">
<div class="align_row permission-explorer">
<div class="bar-filter">
<div><a>Filter:</a></div>
<div><input type="text" class="filter-input"></div>
<div><input type="checkbox" class="filter-granted"><a>Show granted permissions only</a></div>
</div>
<div class="list">
<div class="entry header">
<div>Permission Name</div>
<div>Value</div>
<div>Skip</div>
<div>Negate</div>
<div>Granted</div>
</div>
<div class="entries">
{{for permissions tmpl="#tmpl_permission_entry"/}}
</div>
<!--
<div class="entry">
<div>this_is_a_test_permission</div>
<div>value</div>
<div>skip</div>
<div>negate</div>
<div>granted</div>
</div>
<div class="entry group">
<div class="title"><div class="arrow right"></div><a>Test group</a></div>
<div class="group-entries">
<div class="entry">
<div>Grouped entry A</div>
<div>value</div>
<div>skip</div>
<div>negate</div>
<div>granted</div>
</div>
<div class="entry">
<div>Grouped entry B</div>
<div>value</div>
<div>skip</div>
<div>negate</div>
<div>granted</div>
</div>
</div>
</div>
-->
<div class="overlay-disabled"></div>
</div>
<div>
<button class="button-update"><div class="icon client-check_update"></div> Update</button>
</div>
</div>
</script>
<script id="tmpl_permission_entry" type="text/html">
{{if type == "entry"}}
<div class="entry {{if unset}}unset{{/if}} permission {{>permission_name}}">
<div class="permission-name filter-key">{{>permission_name}}</div>
<div class="permission-value"><node key="{{>permission_name}}_value"></node></div>
<div class="permission-skip"><node key="{{>permission_name}}_skip"></node></div>
<div class="permission-negate"><node key="{{>permission_name}}_negate"></node></div>
<div class="permission-grant"><node key="{{>permission_name}}_grant"></node></div>
</div>
{{else}}
<div class="entry group">
<div class="title"><div class="arrow down"></div><a>{{>name}}</a></div>
<div class="group-entries">
{{for entries tmpl="#tmpl_permission_entry"/}}
</div>
</div>
{{/if}}
</script>
<!-- Music interface -->
<script id="tmpl_music_frame" type="text/html">
<!-- First we want to define some variables if not defined yet. -->
{{if !thumbnail}}
@ -633,7 +815,6 @@
</div>
</div>
</script>
<template id="tmpl_music_frame_empty">
<div class="music-wrapper empty">
<img src="img/music/empty_disk.svg">
@ -752,7 +933,6 @@
</div>
{{/if}}
</script>
<script id="tmpl_selected_music" type="text/html">
<table class="select_info_table">
<tr>
@ -812,7 +992,6 @@
<node key="music_player"/>
</div>
</script>
<script id="tmpl_selected_server" type="text/html">
<div class="select_server">
<table class="select_info_table">