Introducing new bookmark and connection profile system

canary
WolverinDEV 2018-12-28 15:39:23 +01:00
parent 1dbad341b9
commit ac236d1646
24 changed files with 2168 additions and 579 deletions

View File

@ -62,17 +62,6 @@ $background:lightgray;
align-items: center;
border: 2px solid rgba(0, 0, 0, 0);
border-left: 0;
.arrow {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
vertical-align: text-bottom;
}
}
&:hover {
@ -149,5 +138,62 @@ $background:lightgray;
hr:last-child {
display: none;
}
.hidden {
display: none!important;
}
.disabled {
}
.bookmark, .directory {
display: flex!important;
flex-direction: row;
align-items: center;
justify-content: stretch;
.name {
flex-grow: 1;
flex-shrink: 1;
}
.icon, .arrow {
flex-grow: 0;
flex-shrink: 0;
}
.arrow {
margin-right: 5px;
}
}
.directory {
&:hover {
> .sub-container, > .sub-container .sub-menu {
display: block;
}
}
&:not(:hover) {
.sub-container {
display: none;
}
}
.sub-container {
padding-right: 3px;
position: relative;
}
.sub-menu {
display: none;
left: 100%;
top: -13px;
position: absolute;
margin-left: 3px;
}
}
}
}

View File

@ -0,0 +1,141 @@
.modal .modal-bookmarks {
padding: 5px;
display: flex;
flex-direction: column;
justify-content: stretch;
.bookmark-list {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
.list {
display: flex;
flex-direction: column;
justify-content: start;
overflow-y: auto;
.entry {
flex-grow: 1;
flex-shrink: 1;
> .name {
cursor: pointer;
}
&.bookmark {
&.selected {
background-color: #0000FF77;
}
}
&.directory {
&.selected {
> .name {
background-color: #0000FF77;
}
}
> .name {
border: 0 solid gray;
border-bottom: 1px solid #ad9d9d33;
}
.members {
margin-left: 15px;
}
}
}
}
}
.buttons {
flex-grow: 0;
flex-shrink: 0;
margin-top: 5px;
text-align: right;
button {
margin-left: 5px;
}
}
.group_box:not(:first-of-type) {
flex-grow: 0;
flex-shrink: 0;
}
.bookmark-setting {
.group_box {
margin-top: 5px;
}
.property {
display: flex;
flex-direction: row;
justify-content: stretch;
&:not(:first-of-type) {
margin-top: 5px;
}
input, select, .default-channel-container {
flex-grow: 1;
flex-shrink: 1;
}
.default-channel-container {
display: flex;
flex-direction: row;
justify-content: stretch;
button {
margin-left: 5px;
max-width: 120px;
}
}
.key {
width: 160px;
flex-grow: 0;
flex-shrink: 0;
}
}
}
}
.modal .modal-bookmark-create {
.property {
margin-top: 5px;
display: flex;
flex-direction: row;
justify-content: stretch;
.key {
flex-grow: 0;
flex-shrink: 0;
width: 150px;
}
select, input {
flex-grow: 1;
flex-shrink: 1;
}
}
.buttons {
text-align: right;
button {
min-width: 200px;
}
margin-bottom: 5px;
}
}

View File

@ -0,0 +1,28 @@
.modal .modal-connect {
margin-top: 5px;
> div:not(:first-of-type) {
margin-top: 5px;
}
.profile-select-container {
display: flex;
flex-direction: row;
justify-content: space-between;
select {
width: 150px;
}
}
.profile-invalid {
display: flex;
flex-direction: column;
justify-content: start;
> div {
display: inline-flex;
flex-direction: row;
}
}
}

View File

@ -245,3 +245,137 @@
}
}
}
.modal .settings-profiles {
margin: 5px;
> div:not(:first-of-type) {
margin-top: 5px;
}
.profile-status-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.profile-list {
user-select: none;
display: flex;
flex-direction: column;
.list {
display: flex;
flex-direction: column;
justify-content: start;
overflow-y: auto;
border: solid 1px lightgray;
padding: 2px;
background: #33333318;
height: 50%;
min-height: 50%;
max-height: 50%;
.entry {
display: flex;
flex-direction: row;
justify-content: stretch;
cursor: pointer;
&.default {
.name {
font-weight: bold;
}
}
.name {
flex-grow: 1;
flex-shrink: 1;
}
&.selected {
background: #0000FF77;
}
.button {
cursor: pointer;
&:hover {
background-color: #00000010;
}
}
}
}
.management {
width: 100%;
display: flex;
flex-direction: row;
justify-content: stretch;
margin-top: 5px;
float: right;
.space {
flex-grow: 1;
}
button:not(:first-of-type) {
margin-left: 5px;
}
}
}
.general-settings {
display: flex;
flex-direction: column;
justify-content: start;
.setting {
&:not(:first-of-type) {
margin-top: 5px;
}
display: flex;
flex-direction: row;
justify-content: stretch;
.key {
flex-grow: 0;
flex-shrink: 0;
width: 200px;
}
input, div {
flex-grow: 1;
flex-shrink: 1;
}
.select-container {
text-align: right;
}
}
}
.identity-settings {
display: none;
&.active {
display: block;
}
&.identity-settings-teaforo {
/*
.connected, .disconnected {
display: none
}
*/
}
}
}

View File

@ -101,15 +101,21 @@
.group_box {
display: grid;
grid-template-rows: min-content;
display: flex;
flex-direction: column;
justify-content: stretch;
.header {
flex-grow: 0;
flex-shrink: 0;
float: left;
margin-bottom: 2px;
}
.content {
flex-grow: 1;
flex-shrink: 1;
background: rgba(0, 0, 0, .035);
border: lightgray solid 1px;
border-radius: 0 2px;
@ -372,6 +378,7 @@
border: solid black;
border-width: 0 3px 3px 0;
padding: 3px;
height: 10px;
&.right {
transform: rotate(-45deg);

View File

@ -49,6 +49,8 @@
<link rel="stylesheet" href="css/ts/icons.css" type="text/css">
<link rel="stylesheet" href="css/general.css" type="text/css">
<link rel="stylesheet" href="css/modals.css" type="text/css">
<link rel="stylesheet" href="css/modal-bookmarks.css" type="text/css">
<link rel="stylesheet" href="css/modal-connect.css" type="text/css">
<link rel="stylesheet" href="css/modal-query.css" type="text/css">
<link rel="stylesheet" href="css/modal-banlist.css" type="text/css">
<link rel="stylesheet" href="css/modal-bancreate.css" type="text/css">

View File

@ -20,15 +20,14 @@
<div class="icon_x32 client-disconnect"></div>
</div>
<!--
<div class="button-dropdown btn_bookmark" title="{{tr 'Bookmarks' /}}">
<div class="buttons">
<div class="button icon_x32 client-bookmark_manager btn_bookmark_list"></div>
<div class="button-dropdown">
<div class="arrow"></div>
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown bookmark-dropdown" style="width: 300px">
<div class="dropdown bookmark-dropdown" style="width: 350px">
<div class="btn_bookmark_list"><div class="icon client-bookmark_manager"></div><a>{{tr "Manage bookmarks" /}}</a></div>
<div class="btn_bookmark_add"><div class="icon client-bookmark_add"></div><a>{{tr "Add current server to bookmarks" /}}</a></div>
<div class="btn_bookmark_remove"><div class="icon client-bookmark_remove"></div><a>{{tr "Remove current server to bookmarks" /}}</a></div>
@ -36,14 +35,14 @@
</div>
</div>
-->
<div class="divider"></div>
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="buttons">
<div class="button icon_x32 client-away btn_away_toggle"></div>
<div class="button-dropdown">
<div class="arrow"></div>
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
@ -63,7 +62,7 @@
<div class="buttons">
<div class="button icon_x32 client-token btn_token_use"></div>
<div class="button-dropdown">
<div class="arrow"></div>
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
@ -85,7 +84,7 @@
<div class="buttons">
<div class="button icon_x32 client-server_query btn_query_toggle"></div>
<div class="button-dropdown">
<div class="arrow"></div>
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown display_left">
@ -134,7 +133,7 @@
</script>
<!-- Template for the connect modal -->
<script class="jsrender-template" id="tmpl_connect" type="text/html">
<div style="margin-top: 5px;">
<div class="modal-connect">
<div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between">
<div style="width: 68%; margin-bottom: 5px">
<div>{{tr "Remote address and port:" /}}</div>
@ -151,41 +150,15 @@
<div>{{tr "Nickname:" /}}</div>
<input type="text" style="width: 100%" class="connect_nickname" value="">
</div>
<div class="profile-select-container">
<div style="text-align: right;">{{tr "Connection profile:" /}}</div>
<select class="profile-select"> </select>
</div>
<div class="profile-invalid">
<hr>
<div class="group_box">
<div style="display: flex; justify-content: space-between;">
<div style="text-align: right;">{{tr "Identity Settings" /}}</div>
<select class="identity_select">
<option name="identity_type" value="TEAFORO">{{tr "Forum Account" /}}</option>
<option name="identity_type" value="TEAMSPEAK">{{tr "TeamSpeak" /}}</option>
<option name="identity_type" value="NICKNAME">{{tr "Nickname (Debug purposes only!)" /}}</option> <!-- Only available on localhost for debug -->
</select>
<div>The selected connect profile is invalid</div>
<div><div>Click&nbsp;</div><a href="#" class="button-manage-profiles">here</a><div>&nbsp;to manage your connect profiles</div></div>
</div>
<hr>
<div class="identity_config_TEAMSPEAK identity_config">
{{tr "Please enter your exported TS3 Identity string bellow or select your exported Identity"/}}<br>
<div style="width: 100%; display: flex; justify-content: stretch; flex-direction: row">
<input placeholder="Identity string" style="min-width: 60%; flex-shrink: 1; flex-grow: 2; margin: 5px;" class="identity_string">
<div style="max-width: 200px; flex-grow: 1; flex-shrink: 4; margin: 5px"><input style="display: flex; width: 100%;" class="identity_file" type="file"></div>
</div>
</div>
<div class="identity_config_TEAFORO identity_config">
<div class="connected">
{{tr "You're using your forum account as verification"/}}
</div>
<div class="disconnected">
<!-- TODO tr -->
You cant use your TeaSpeak forum account.<br>
You're not connected!<br>
Click {{if !client}}<a href="{{:forum_path}}login.php">here</a>{{else}}<a href="#" class="native-teaforo-login">here</a>{{/if}} to login via the TeaSpeak forum.
</div>
</div>
<div class="identity_config_NICKNAME identity_config">
{{tr "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>
@ -701,7 +674,7 @@
<div class="settings-translations">
<div class="group_box">
<div class="header">{{tr "Available translations" /}}</div>
<div class="content settings-microphone">
<div class="content">
<div class="setting-list">
<div class="list">
<!--
@ -732,9 +705,115 @@
</div>
</x-content>
</x-entry>
<x-entry>
<x-tag class="tab-profiles">
{{tr "Profiles" /}}
</x-tag>
<x-content>
<div class="settings-profiles">
<div class="group_box">
<div class="header">{{tr "Available profiles" /}}</div>
<div class="content">
<div class="profile-list">
<div class="list">
<!--
<div class="entry default">{{tr "English (Default / Fallback)" /}}</div>
<div class="entry repository">
<div class="name">TeaSpeak Official</div>
<div class="button button-delete"><div class="icon client-delete"></div></div>
<div class="button button-info"><div class="icon client-about"></div></div>
</div>
<div class="entry translation selected">
<div class="name">German (Google Translate)</div>
<div class="button button-info"><div class="icon client-about"></div></div>
</div>
-->
</div>
<div class="management">
<button class="button-set-default">{{tr "Set selected as default" /}}</button>
<button class="button-delete">{{tr "Delete selected" /}}</button>
<div class="space"></div>
<button class="button-add-profile">{{tr "Create profile" /}}</button>
</div>
</div>
</div>
</div>
<div class="group_box">
<div class="header">{{tr "Profile settings" /}}</div>
<div class="content">
<div class="profile-settings">
<div class="general-settings">
<div class="setting">
<div class="key">{{tr "Profile name:" /}}</div>
<input class="value setting-name">
</div>
<div class="setting">
<div class="key">{{tr "Default nickname:" /}}</div>
<input class="value setting-default-nickname">
</div>
<div class="setting">
<div class="key">{{tr "Default server password:" /}}</div>
<input class="value setting-default-password">
</div>
<div class="setting">
<div class="key">{{tr "Identify Type:" /}}</div>
<div class="select-container">
<select>
<option name="identity-type" value="teaforo">{{tr "Forum Account" /}}</option>
<option name="identity-type" value="teamspeak">{{tr "TeamSpeak" /}}</option>
<option name="identity-type" value="nickname">{{tr "Nickname (Debug purposes only!)" /}}</option> <!-- Only available on localhost for debug -->
</select>
</div>
</div>
</div>
<hr>
<div class="identity-settings identity-settings-teamspeak">
{{tr "Please enter your exported TS3 Identity string bellow or select your exported Identity"/}}<br>
<div style="width: 100%; display: flex; justify-content: stretch; flex-direction: row">
<input placeholder="Identity string" style="min-width: 60%; flex-shrink: 1; flex-grow: 2; margin: 5px;" class="identity_string">
<div style="max-width: 200px; flex-grow: 1; flex-shrink: 4; margin: 5px"><input style="display: flex; width: 100%;" class="identity_file" type="file"></div>
</div>
<div class="error-message">
</div>
</div>
<div class="identity-settings identity-settings-teaforo">
<div class="connected">
{{tr "You're using your forum account as verification"/}}
</div>
<div class="disconnected">
<!-- TODO tr -->
You cant use your TeaSpeak forum account. You're not connected!<br>
Click {{if !client}}<a href="{{:forum_path}}login.php">here</a>{{else}}<a href="#" class="native-teaforo-login">here</a>{{/if}} to login via the TeaSpeak forum.
</div>
</div>
<div class="identity-settings identity-settings-nickname">
<a>
{{tr "This is just for debug and uses the name as unique identifier" /}}
</a>
<div>
<a>{{tr "Username:" /}}</a>
<input class="setting-name" placeholder="WolverinDEV">
</div>
</div>
</div>
</div>
</div>
</div>
</x-content>
</x-entry>
</x-tab>
</script>
<script class="jsrender-template" id="settings-profile-list-entry" type="text/html">
<div class="entry profile {{if id == 'default'}}default{{/if}}">
<div class="name">{{>profile_name}}</div>
<!-- <div class="button button-delete"><div class="icon client-delete"></div></div> -->
<div title="{{tr 'Profile is valid' /}}" class="icon client-delete status status-invalid"></div>
<div title="{{tr 'Profile is invalid' /}}" class="icon client-apply status status-valid"></div>
</div>
</script>
<script class="jsrender-template" id="settings-translations-list-entry" type="text/html">
{{if type == "repository" }}
<div class="entry repository" repository="{{:id}}">
@ -1106,7 +1185,7 @@
</div>
</div>
</script>
9
<script class="jsrender-template" id="tmpl_client_ban" type="text/html">
<div class="align_column">
<div class="align_column" style="margin: 5px">
@ -1767,5 +1846,123 @@
<div class="column column-bound-server">{{>bounded_server}}</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_manage_bookmarks" type="text/html">
<div class="modal-bookmarks">
<div class="group_box">
<div class="header">{{tr "Bookmarks" /}}</div>
<div class="content bookmark-list">
<div class="list">
<div class="entry bookmark">
<div class="name">TeaSpeak official</div>
</div>
</div>
</div>
</div>
<div class="buttons">
<button class="button-create">{{tr "Create new bookmark/directory" /}}</button>
<button class="button-delete">{{tr "Delete selected bookmark/directory" /}}</button>
</div>
<hr>
<div class="group_box">
<div class="header">{{tr "Bookmark settings" /}}</div>
<div class="content">
<div class="bookmark-setting bookmark-setting-bookmark">
<div class="property">
<div class="key">{{tr "Bookmark name:" /}}</div>
<input class="setting-bookmark-name">
</div>
<div class="property">
<div class="key">{{tr "Connect profile:" /}}</div>
<select class="setting-bookmark-profile"></select>
</div>
<div class="group_box">
<div class="header">{{tr "Server Properties" /}}</div>
<div class="content">
<div class="property">
<div class="key">{{tr "Server address:" /}}</div>
<input class="setting-server-host">
</div>
<div class="property">
<div class="key">{{tr "Server Port:" /}}</div>
<input class="setting-server-port" type="number">
</div>
<div class="property">
<div class="key">{{tr "Server Password:" /}}</div>
<input class="setting-server-password" type="password" id="bookmark_server_password_{{rnd '0~13377331'/}}">
</div>
</div>
</div>
<div class="group_box">
<div class="header">{{tr "Connect Properties (Not yet supported)" /}}</div>
<div class="content">
<div class="property">
<div class="key">{{tr "Username:" /}}</div>
<input class="setting-username" disabled>
</div>
<div class="property">
<div class="key">{{tr "Default channel:" /}}</div>
<div class="default-channel-container">
<input class="setting-channel" disabled>
<button class="button-set-to-current" disabled>{{tr "current channel" /}}</button>
</div>
</div>
<div class="property">
<div class="key">{{tr "Channel password:" /}}</div>
<input class="setting-channel-password" type="password" id="bookmark_channel_password_{{rnd '0~13377331'/}}" disabled>
</div>
</div>
</div>
</div>
<div class="bookmark-setting bookmark-setting-directory">
<div class="property">
<div class="key">{{tr "Directory name:" /}}</div>
<input class="setting-bookmark-name">
</div>
</div>
</div>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_manage_bookmarks-list_entry" type="text/html">
{{if type == "bookmark" }}
<div class="entry bookmark">
<div class="name">{{>name}}</div>
</div>
{{else type == "directory" }}
<div class="entry directory">
<div class="name">{{>name}}</div>
<div class="members"> </div>
</div>
{{/if}}
</script>
<script class="jsrender-template" id="tmpl_manage_bookmarks-create" type="text/html">
<div class="modal-bookmark-create">
<div class="property">
<div class="key">Bookmark Type:</div>
<select class="bookmark-type">
<option value="bookmark">Bookmark</option>
<option value="directory">Directory</option>
</select>
</div>
<div class="property">
<div class="key">Parent Directory:</div>
<select class="bookmark-parent">
<option bookmark-uuid=""></option>
</select>
</div>
<div class="property">
<div class="key">Bookmark Name:</div>
<input class="bookmark-name">
</div>
<hr>
<div class="buttons">
<button class="button-create">Create</button>
</div>
</div>
</script>
</body>
</html>

View File

@ -1,159 +0,0 @@
enum IdentitifyType {
TEAFORO,
TEAMSPEAK,
NICKNAME
}
namespace TSIdentityHelper {
export let funcationParseIdentity: any;
export let funcationParseIdentityByFile: any;
export let funcationCalculateSecurityLevel: any;
export let functionUid: any;
export let funcationExportIdentity: any;
export let funcationPublicKey: any;
export let funcationSignMessage: any;
let functionLastError: any;
let functionClearLastError: any;
let functionDestroyString: any;
let functionDestroyIdentity: any;
export function setup() : boolean {
functionDestroyString = Module.cwrap("destroy_string", "pointer", []);
functionLastError = Module.cwrap("last_error_message", null, ["string"]);
funcationParseIdentity = Module.cwrap("parse_identity", "pointer", ["string"]);
funcationParseIdentityByFile = Module.cwrap("parse_identity_file", "pointer", ["string"]);
functionDestroyIdentity = Module.cwrap("delete_identity", null, ["pointer"]);
funcationCalculateSecurityLevel = Module.cwrap("identity_security_level", "pointer", ["pointer"]);
funcationExportIdentity = Module.cwrap("identity_export", "pointer", ["pointer"]);
funcationPublicKey = Module.cwrap("identity_key_public", "pointer", ["pointer"]);
funcationSignMessage = Module.cwrap("identity_sign", "pointer", ["pointer", "string", "number"]);
functionUid = Module.cwrap("identity_uid", "pointer", ["pointer"]);
return Module.cwrap("tomcrypt_initialize", "number", [])() == 0;
}
export function last_error() : string {
return unwarpString(functionLastError());
}
export function unwarpString(str) : string {
if(str == "") return "";
try {
if(!$.isFunction(window.Pointer_stringify)) {
displayCriticalError(tr("Missing required wasm function!<br>Please reload the page!"));
}
let message: string = window.Pointer_stringify(str);
functionDestroyString(str);
return message;
} catch (error) {
console.error(error);
return "";
}
}
export function loadIdentity(key: string) : TeamSpeakIdentity {
let handle = funcationParseIdentity(key);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
}
export function loadIdentityFromFileContains(contains: string) : TeamSpeakIdentity {
let handle = funcationParseIdentityByFile(contains);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
}
}
interface Identity {
name() : string;
uid() : string;
type() : IdentitifyType;
valid() : boolean;
}
class NameIdentity implements Identity {
private _name: string;
constructor(name: string) {
this._name = name;
}
set_name(name: string) { this._name = name; }
name(): string {
return this._name;
}
uid(): string {
return btoa(this._name); //FIXME hash!
}
type(): IdentitifyType {
return IdentitifyType.NICKNAME;
}
valid(): boolean {
return this._name != undefined && this._name != "";
}
}
class TeamSpeakIdentity implements Identity {
private handle: any;
private _name: string;
constructor(handle: any, name: string) {
this.handle = handle;
this._name = name;
}
securityLevel() : number | undefined {
return parseInt(TSIdentityHelper.unwarpString(TSIdentityHelper.funcationCalculateSecurityLevel(this.handle)));
}
name() : string { return this._name; }
uid() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.functionUid(this.handle));
}
type() : IdentitifyType { return IdentitifyType.TEAMSPEAK; }
signMessage(message: string): string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationSignMessage(this.handle, message, message.length));
}
exported() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationExportIdentity(this.handle));
}
publicKey() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationPublicKey(this.handle));
}
valid() : boolean { return true; }
}
class TeaForumIdentity implements Identity {
readonly identityData: string;
readonly identityDataJson: string;
readonly identitySign: string;
valid() : boolean {
return this.identityData.length > 0 && this.identityDataJson.length > 0 && this.identitySign.length > 0;
}
constructor(data: string, sign: string) {
this.identityDataJson = data;
this.identityData = JSON.parse(this.identityDataJson);
this.identitySign = sign;
}
name() : string { return this.identityData["user_name"]; }
uid() : string { return "TeaForo#" + this.identityData["user_id"]; }
type() : IdentitifyType { return IdentitifyType.TEAFORO; }
}

View File

@ -1,21 +1,14 @@
namespace bookmarks {
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
return Math
.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}
export interface ConnectIdentity {
identity_type: IdentitifyType;
}
export interface ForumConnectIdentity extends ConnectIdentity { }
export interface NicknameConnectIdentity extends ConnectIdentity { }
export interface TeamSpeakConnectIdentity extends ConnectIdentity { }
export interface ServerProperties {
server_address: string;
server_port: number;
@ -41,6 +34,7 @@ namespace bookmarks {
default_channel_password_hash?: string;
default_channel_password?: string;
connect_profile: string;
}
export interface DirectoryBookmark {
@ -104,12 +98,28 @@ namespace bookmarks {
return find_bookmark_recursive(bookmarks(), uuid);
}
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
while(!books.length) {
const directory = books.pop_front();
if(directory.type == BookmarkType.DIRECTORY) {
const cast = <DirectoryBookmark>directory;
if(cast.content.indexOf(bookmark) != -1)
return cast;
books.push(...cast.content);
}
}
return bookmarks();
}
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
const bookmark = {
display_name: display_name,
server_properties: server_properties,
nickname: nickname,
type: BookmarkType.ENTRY,
connect_profile: "default",
unique_id: guid()
} as Bookmark;
@ -132,7 +142,7 @@ namespace bookmarks {
//TODO test if the new parent is within the old bookmark
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark(bookmark)
delete_bookmark(bookmark);
parent.content.push(bookmark)
}

View File

@ -29,6 +29,7 @@ namespace MessageHelper {
return this.formatElement("<unknwon object>");
} else if(typeof(object) === "function") return this.formatElement(object());
else if(typeof(object) === "undefined") return this.formatElement("<undefined>");
else if(typeof(object) === "number") return [$.spawn("a").text(object)];
return this.formatElement("<unknown object type " + typeof object + ">");
}

View File

@ -18,6 +18,7 @@ enum DisconnectReason {
CONNECTION_PING_TIMEOUT,
CLIENT_KICKED,
CLIENT_BANNED,
HANDSHAKE_FAILED,
SERVER_CLOSED,
SERVER_REQUIRES_PASSWORD,
UNKNOWN
@ -76,7 +77,7 @@ class TSClient {
this.controlBar.initialise();
}
startConnection(addr: string, identity: Identity, name?: string, password?: {password: string, hashed: boolean}) {
startConnection(addr: string, profile: profiles.ConnectionProfile, name?: string, password?: {password: string, hashed: boolean}) {
if(this.serverConnection)
this.handleDisconnect(DisconnectReason.REQUESTED);
@ -96,12 +97,12 @@ class TSClient {
if(password && !password.hashed) {
helpers.hashPassword(password.password).then(password => {
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password));
this.serverConnection.startConnection({host, port}, new HandshakeHandler(profile, name, password));
}).catch(error => {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
})
} else
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password ? password.password : undefined));
this.serverConnection.startConnection({host, port}, new HandshakeHandler(profile, name, password ? password.password : undefined));
}
@ -141,6 +142,13 @@ class TSClient {
}
private certAcceptUrl() {
//TODO here
const properties = {
connect_direct: true,
connect_profile: this.serverConnection._handshakeHandler.profile.id,
connect_url: this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port
};
// document.URL
let callback = document.URL;
if(document.location.search.length == 0)
@ -148,7 +156,11 @@ class TSClient {
else
callback += "&default_connect_url=true";
//
switch (this.serverConnection._handshakeHandler.identity.type()) {
//this.serverConnection._handshakeHandler.profile
callback += "&connect_profile=" + encodeURIComponent(this.serverConnection._handshakeHandler.profile.id);
/*
switch (this.serverConnection._handshakeHandler.profile.type()) {
case IdentitifyType.TEAFORO:
callback += "&default_connect_type=teaforo";
break;
@ -156,6 +168,7 @@ class TSClient {
callback += "&default_connect_type=teamspeak";
break;
}
*/
callback += "&default_connect_url=" + encodeURIComponent(this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port);
return "https://" + this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port + "/?forward_url=" + encodeURIComponent(callback);
@ -166,8 +179,7 @@ class TSClient {
case DisconnectReason.REQUESTED:
break;
case DisconnectReason.CONNECT_FAILURE:
console.error(tr("Could not connect to remote host! Exception"));
console.error(data);
console.error(tr("Could not connect to remote host! Exception: %o"), data);
if(native_client) {
createErrorModal(
@ -185,6 +197,14 @@ class TSClient {
}
sound.play(Sound.CONNECTION_REFUSED);
break;
case DisconnectReason.HANDSHAKE_FAILED:
//TODO sound
console.error(tr("Failed to process handshake: %o"), data);
createErrorModal(
tr("Could not connect"),
tr("Failed to process handshake: ") + data as string
).open();
break;
case DisconnectReason.CONNECTION_CLOSED:
console.error(tr("Lost connection to remote server!"));
createErrorModal(
@ -215,7 +235,7 @@ class TSClient {
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return;
this.startConnection(this.serverConnection._remote_address.host + ":" + this.serverConnection._remote_address.port,
this.serverConnection._handshakeHandler.identity,
this.serverConnection._handshakeHandler.profile,
this.serverConnection._handshakeHandler.name,
{password: password as string, hashed: false});
}).open();

View File

@ -277,59 +277,50 @@ class ServerConnection {
}
}
class HandshakeHandler {
readonly identity: Identity;
readonly name?: string;
private connection: ServerConnection;
server_password: string;
interface HandshakeIdentityHandler {
connection: ServerConnection;
constructor(identity: Identity, name?: string, password?: string) {
this.identity = identity;
start_handshake();
register_callback(callback: (success: boolean, message?: string) => any);
}
class HandshakeHandler {
private connection: ServerConnection;
private handshake_handler: HandshakeIdentityHandler;
readonly profile: profiles.ConnectionProfile;
readonly name: string;
readonly server_password: string;
constructor(profile: profiles.ConnectionProfile, name: string, password: string) {
this.profile = profile;
this.server_password = password;
this.name = name;
}
setConnection(con: ServerConnection) {
this.connection = con;
this.connection.commandHandler["handshakeidentityproof"] = this.handleCommandHandshakeIdentityProof.bind(this);
}
startHandshake() {
let data: any = {
intention: 0,
authentication_method: this.identity.type()
};
if(this.identity.type() == IdentitifyType.TEAMSPEAK) {
data.publicKey = (this.identity as TeamSpeakIdentity).publicKey();
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
data.data = (this.identity as TeaForumIdentity).identityDataJson;
} else if(this.identity.type() == IdentitifyType.NICKNAME) {
data["client_nickname"] = this.identity.name();
this.handshake_handler = this.profile.spawn_identity_handshake_handler(this.connection);
if(!this.handshake_handler) {
this.handshake_failed("failed to create identity handler");
return;
}
this.connection.sendCommand("handshakebegin", data).catch(error => {
console.log(error);
//TODO here
}).then(() => {
if(this.identity.type() == IdentitifyType.NICKNAME) {
this.handshake_handler.register_callback((flag, message) => {
if(flag)
this.handshake_finished();
}
else
this.handshake_failed(message);
});
this.handshake_handler.start_handshake();
}
private handleCommandHandshakeIdentityProof(json) {
let proof: string;
if(this.identity.type() == IdentitifyType.TEAMSPEAK) {
proof = (this.identity as TeamSpeakIdentity).signMessage(json[0]["message"]);
} else if(this.identity.type() == IdentitifyType.TEAFORO) {
proof = (this.identity as TeaForumIdentity).identitySign;
} else if(this.identity.type() == IdentitifyType.NICKNAME) {
//FIXME handle error this should never happen!
}
this.connection.sendCommand("handshakeindentityproof", {proof: proof}).catch(error => {
console.error(tr("Got login error"));
console.log(error);
}).then(() => this.handshake_finished()); //TODO handle error
private handshake_failed(message: string) {
this.connection._client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message);
}
private handshake_finished(version?: string) {
@ -348,7 +339,7 @@ class HandshakeHandler {
const browser_name = (navigator.browserSpecs || {})["name"] || " ";
let data = {
//TODO variables!
client_nickname: this.name ? this.name : this.identity.name(),
client_nickname: this.name,
client_platform: (browser_name ? browser_name + " " : "") + navigator.platform,
client_version: "TeaWeb " + git_version + " (" + navigator.userAgent + ")",

View File

@ -110,14 +110,19 @@ function load_script(path: string | string[]) : Promise<Boolean> {
});
} else {
return new Promise((resolve, reject) => {
const tag = document.createElement("script");
const tag: HTMLScriptElement = document.createElement("script");
tag.type = "application/javascript";
tag.async = true;
tag.defer = true;
tag.onerror = error => {
console.log(error);
tag.remove();
reject(error);
};
tag.onload = () => resolve();
tag.onload = () => {
console.debug("Script %o loaded", path);
resolve();
};
document.getElementById("scripts").appendChild(tag);
tag.src = path;
});
@ -179,9 +184,14 @@ function loadDebug() {
"js/crypto/sha.js",
"js/crypto/hex.js",
//load the profiles
"js/profiles/ConnectionProfile.js",
"js/profiles/Identity.js",
//Load UI
"js/ui/modal/ModalQuery.js",
"js/ui/modal/ModalQueryManage.js",
"js/ui/modal/ModalBookmarks.js",
"js/ui/modal/ModalConnect.js",
"js/ui/modal/ModalSettings.js",
"js/ui/modal/ModalCreateChannel.js",
@ -227,12 +237,14 @@ function loadDebug() {
"js/FileManager.js",
"js/client.js",
"js/chat.js",
"js/Identity.js",
"js/PPTListener.js",
...custom_scripts
]).then(() => load_wait_scripts([
"js/codec/CodecWrapperWorker.js"
"js/codec/CodecWrapperWorker.js",
"js/profiles/identities/NameIdentity.js", //Depends on Identity
"js/profiles/identities/TeaForumIdentity.js", //Depends on Identity
"js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity
])).then(() => load_wait_scripts([
"js/main.js"
])).then(() => {

View File

@ -1,6 +1,5 @@
/// <reference path="chat.ts" />
/// <reference path="client.ts" />
/// <reference path="Identity.ts" />
/// <reference path="utils/modal.ts" />
/// <reference path="ui/modal/ModalConnect.ts" />
/// <reference path="ui/modal/ModalCreateChannel.ts" />
@ -15,8 +14,6 @@ let settings: Settings;
let globalClient: TSClient;
let chat: ChatBox;
let forumIdentity: TeaForumIdentity;
const js_render = window.jsrender || $;
const native_client = window.require !== undefined;
@ -27,9 +24,11 @@ function getUserMediaFunction() {
}
function setup_close() {
if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) return;
window.onbeforeunload = event => {
if(profiles.requires_save())
profiles.save();
if(!settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) {
if(!globalClient.serverConnection || !globalClient.serverConnection.connected) return;
if(!native_client) {
@ -53,6 +52,7 @@ function setup_close() {
}
});
}
}
};
}
@ -125,10 +125,11 @@ async function initialize() {
}
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) {
if(!profiles.identities.setup_teamspeak()) {
console.error(tr("Could not setup the TeamSpeak identity parser!"));
return;
}
profiles.load();
try {
await ppt.initialize();
@ -145,9 +146,7 @@ function main() {
settings = new Settings();
globalClient = new TSClient();
/** Setup the XF forum identity **/
if(settings.static("forum_user_data")) {
forumIdentity = new TeaForumIdentity(settings.static("forum_user_data"), settings.static("forum_user_sign"));
}
profiles.identities.setup_forum();
chat = new ChatBox($("#chat"));
globalClient.setup();
@ -160,6 +159,8 @@ function main() {
//Modals.spawnSettingsModal();
//Modals.createChannelModal(undefined);
/*
//FIXME
if(settings.static("default_connect_url")) {
switch (settings.static("default_connect_type")) {
case "teaforo":

View File

@ -0,0 +1,200 @@
namespace profiles {
export class ConnectionProfile {
id: string;
profile_name: string;
default_username: string;
default_password: string;
selected_identity_type: string = "unset";
identities: {[key:string]:identities.Identity} = {};
constructor(id: string) {
this.id = id;
}
selected_identity() : identities.Identity {
const current_type = this.selected_type();
if(current_type === undefined)
return undefined;
if(current_type == identities.IdentitifyType.TEAFORO) {
return identities.static_forum_identity();
} else if(current_type == identities.IdentitifyType.TEAMSPEAK || current_type == identities.IdentitifyType.NICKNAME) {
return this.identities[this.selected_identity_type.toLowerCase()];
}
return undefined;
}
selected_type?() : identities.IdentitifyType {
return identities.IdentitifyType[this.selected_identity_type.toUpperCase()];
}
set_identity(type: identities.IdentitifyType, identity: identities.Identity) {
this.identities[identities.IdentitifyType[type].toLowerCase()] = identity;
}
spawn_identity_handshake_handler?(connection: ServerConnection) : HandshakeIdentityHandler {
const identity = this.selected_identity();
if(!identity)
return undefined;
return identity.spawn_identity_handshake_handler(connection);
}
encode?() : string {
const identity_data = {};
for(const key in this.identities)
if(this.identities[key])
identity_data[key] = this.identities[key].encode();
return JSON.stringify({
version: 1,
username: this.default_username,
password: this.default_password,
profile_name: this.profile_name,
identity_type: this.selected_identity_type,
identity_data: identity_data,
id: this.id
});
}
valid() : boolean {
return this.selected_identity() !== undefined && this.default_username !== undefined;
}
}
function decode_profile(data) : ConnectionProfile | string {
data = JSON.parse(data);
if(data.version !== 1)
return "invalid version";
const result: ConnectionProfile = new ConnectionProfile(data.id);
result.default_username = data.username;
result.default_password = data.password;
result.profile_name = data.profile_name;
result.selected_identity_type = (data.identity_type || "").toLowerCase();
if(data.identity_data) {
for(const key in data.identity_data) {
const type = identities.IdentitifyType[key.toUpperCase() as string];
const _data = data.identity_data[key];
if(type == undefined) continue;
const identity = identities.decode_identity(type, _data);
if(identity == undefined) continue;
result.identities[key.toLowerCase()] = identity;
}
}
return result;
}
interface ProfilesData {
version: number;
profiles: string[];
}
let available_profiles: ConnectionProfile[] = [];
export function load() {
available_profiles = [];
const profiles_json = localStorage.getItem("profiles");
let profiles_data: ProfilesData = profiles_json ? JSON.parse(profiles_json) : {version: 0} as any;
if(profiles_data.version === 0) {
profiles_data = {
version: 1,
profiles: []
};
}
if(profiles_data.version == 1) {
for(const profile_data of profiles_data.profiles) {
const profile = decode_profile(profile_data);
if(typeof(profile) === 'string') {
console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data);
continue;
}
available_profiles.push(profile);
}
}
if(!find_profile("default")) { //Create a default profile
const profile = create_new_profile("default","default");
profile.default_password = "";
profile.default_username = "Another TeaSpeak user";
profile.profile_name = "Default Profile";
save();
}
}
export function create_new_profile(name: string, id?: string) : ConnectionProfile {
const profile = new ConnectionProfile(id || guid());
profile.profile_name = name;
profile.default_username = "Another TeaSpeak user";
available_profiles.push(profile);
return profile;
}
let _requires_save = false;
export function save() {
const profiles: string[] = [];
for(const profile of available_profiles)
profiles.push(profile.encode());
const data = JSON.stringify({
version: 1,
profiles: profiles
});
localStorage.setItem("profiles", data);
}
export function mark_need_save() {
_requires_save = true;
}
export function requires_save() : boolean {
return _requires_save;
}
export function profiles() : ConnectionProfile[] {
return available_profiles;
}
export function find_profile(id: string) : ConnectionProfile | undefined {
for(const profile of profiles())
if(profile.id == id)
return profile;
return undefined;
}
export function find_profile_by_name(name: string) : ConnectionProfile | undefined {
name = name.toLowerCase();
for(const profile of profiles())
if((profile.profile_name || "").toLowerCase() == name)
return profile;
return undefined;
}
export function default_profile() : ConnectionProfile {
return find_profile("default");
}
export function set_default_profile(profile: ConnectionProfile) {
const old_default = default_profile();
if(old_default && old_default != profile) {
old_default.id = guid();
}
profile.id = "default";
}
export function delete_profile(profile: ConnectionProfile) {
available_profiles.remove(profile);
}
}

View File

@ -0,0 +1,84 @@
namespace profiles.identities {
export enum IdentitifyType {
TEAFORO,
TEAMSPEAK,
NICKNAME
}
export interface Identity {
name() : string;
uid() : string;
type() : IdentitifyType;
valid() : boolean;
encode?() : string;
decode(data: string) : boolean;
spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler;
}
export function decode_identity(type: IdentitifyType, data: string) : Identity {
let identity: Identity;
switch (type) {
case IdentitifyType.NICKNAME:
identity = new NameIdentity();
break;
case IdentitifyType.TEAFORO:
identity = new TeaForumIdentity(undefined, undefined);
break;
case IdentitifyType.TEAMSPEAK:
identity = new TeamSpeakIdentity(undefined, undefined);
break;
}
if(!identity)
return undefined;
if(!identity.decode(data))
return undefined;
return identity;
}
export function create_identity(type: IdentitifyType) {
let identity: Identity;
switch (type) {
case IdentitifyType.NICKNAME:
identity = new NameIdentity();
break;
case IdentitifyType.TEAFORO:
identity = new TeaForumIdentity(undefined, undefined);
break;
case IdentitifyType.TEAMSPEAK:
identity = new TeamSpeakIdentity(undefined, undefined);
break;
}
return identity;
}
export abstract class AbstractHandshakeIdentityHandler implements HandshakeIdentityHandler {
connection: ServerConnection;
protected callbacks: ((success: boolean, message?: string) => any)[] = [];
protected constructor(connection: ServerConnection) {
this.connection = connection;
}
register_callback(callback: (success: boolean, message?: string) => any) {
this.callbacks.push(callback);
}
abstract start_handshake();
protected trigger_success() {
for(const callback of this.callbacks)
callback(true);
}
protected trigger_fail(message: string) {
for(const callback of this.callbacks)
callback(false, message);
}
}
}

View File

@ -0,0 +1,74 @@
/// <reference path="../Identity.ts" />
namespace profiles.identities {
class NameHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: NameIdentity;
constructor(connection: ServerConnection, identity: profiles.identities.NameIdentity) {
super(connection);
this.identity = identity;
}
start_handshake() {
this.connection.commandHandler["handshakeidentityproof"] = () => this.trigger_fail("server requested unexpected proof");
this.connection.sendCommand("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
client_nickname: this.identity.name()
}).catch(error => {
console.error(tr("Failed to initialize name based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");
}).then(() => this.trigger_success());
}
}
export class NameIdentity implements Identity {
private _name: string;
constructor(name?: string) {
this._name = name;
}
set_name(name: string) { this._name = name; }
name(): string {
return this._name;
}
uid(): string {
return btoa(this._name); //FIXME hash!
}
type(): IdentitifyType {
return IdentitifyType.NICKNAME;
}
valid(): boolean {
return this._name != undefined && this._name != "";
}
decode(data) {
data = JSON.parse(data);
if(data.version !== 1)
return false;
this._name = data["name"];
return true;
}
encode?() : string {
return JSON.stringify({
version: 1,
name: this._name
});
}
spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler {
return new NameHandshakeHandler(connection, this);
}
}
}

View File

@ -0,0 +1,105 @@
/// <reference path="../Identity.ts" />
namespace profiles.identities {
class TeaForumHandshakeHandler extends AbstractHandshakeIdentityHandler {
readonly identity: TeaForumIdentity;
constructor(connection: ServerConnection, identity: profiles.identities.TeaForumIdentity) {
super(connection);
this.identity = identity;
}
start_handshake() {
this.connection.commandHandler["handshakeidentityproof"] = this.handle_proof.bind(this);
this.connection.sendCommand("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
data: this.identity.data_json()
}).catch(error => {
console.error(tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");
});
}
private handle_proof(json) {
this.connection.sendCommand("handshakeindentityproof", {
proof: this.identity.data_sign()
}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute proof (" + error + ")");
}).then(() => this.trigger_success());
}
}
export class TeaForumIdentity implements Identity {
private identityData: string;
private identityDataJson: string;
private identitySign: string;
valid() : boolean {
return this.identityData.length > 0 && this.identityDataJson.length > 0 && this.identitySign.length > 0;
}
constructor(data: string, sign: string) {
this.identityDataJson = data;
this.identityData = data ? JSON.parse(this.identityDataJson) : undefined;
this.identitySign = sign;
}
data_json() : string { return this.identityDataJson; }
data_sign() : string { return this.identitySign; }
name() : string { return this.identityData["user_name"]; }
uid() : string { return "TeaForo#" + this.identityData["user_id"]; }
type() : IdentitifyType { return IdentitifyType.TEAFORO; }
decode(data) {
data = JSON.parse(data);
if(data.version !== 1)
return false;
this.identityDataJson = data["identity_data"];
this.identitySign = data["identity_sign"];
this.identityData = JSON.parse(this.identityData);
return true;
}
encode?() : string {
return JSON.stringify({
version: 1,
identity_data: this.identityDataJson,
identity_sign: this.identitySign
});
}
spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler {
return new TeaForumHandshakeHandler(connection, this);
}
}
let static_identity: TeaForumIdentity;
export function setup_forum() {
const user_data = settings.static("forum_user_data") as string;
const user_sign = settings.static("forum_user_sign") as string;
if(user_data && user_sign)
static_identity = new TeaForumIdentity(user_data, user_sign);
}
export function valid_static_forum_identity() : boolean {
return static_identity && static_identity.valid();
}
export function static_forum_identity() : TeaForumIdentity | undefined {
return static_identity;
}
}

View File

@ -0,0 +1,179 @@
/// <reference path="../Identity.ts" />
namespace profiles.identities {
export namespace TSIdentityHelper {
export let funcationParseIdentity: any;
export let funcationParseIdentityByFile: any;
export let funcationCalculateSecurityLevel: any;
export let functionUid: any;
export let funcationExportIdentity: any;
export let funcationPublicKey: any;
export let funcationSignMessage: any;
let functionLastError: any;
let functionClearLastError: any;
let functionDestroyString: any;
let functionDestroyIdentity: any;
export function setup() : boolean {
functionDestroyString = Module.cwrap("destroy_string", "pointer", []);
functionLastError = Module.cwrap("last_error_message", null, ["string"]);
funcationParseIdentity = Module.cwrap("parse_identity", "pointer", ["string"]);
funcationParseIdentityByFile = Module.cwrap("parse_identity_file", "pointer", ["string"]);
functionDestroyIdentity = Module.cwrap("delete_identity", null, ["pointer"]);
funcationCalculateSecurityLevel = Module.cwrap("identity_security_level", "pointer", ["pointer"]);
funcationExportIdentity = Module.cwrap("identity_export", "pointer", ["pointer"]);
funcationPublicKey = Module.cwrap("identity_key_public", "pointer", ["pointer"]);
funcationSignMessage = Module.cwrap("identity_sign", "pointer", ["pointer", "string", "number"]);
functionUid = Module.cwrap("identity_uid", "pointer", ["pointer"]);
return Module.cwrap("tomcrypt_initialize", "number", [])() == 0;
}
export function last_error() : string {
return unwarpString(functionLastError());
}
export function unwarpString(str) : string {
if(str == "") return "";
try {
if(!$.isFunction(window.Pointer_stringify)) {
displayCriticalError(tr("Missing required wasm function!<br>Please reload the page!"));
}
let message: string = window.Pointer_stringify(str);
functionDestroyString(str);
return message;
} catch (error) {
console.error(error);
return "";
}
}
export function loadIdentity(key: string) : TeamSpeakIdentity {
let handle = funcationParseIdentity(key);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
}
export function loadIdentityFromFileContains(contains: string) : TeamSpeakIdentity {
let handle = funcationParseIdentityByFile(contains);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
}
export function load_identity(handle: TeamSpeakIdentity, key) : boolean {
let native_handle = funcationParseIdentity(key);
if(!native_handle) return false;
handle["handle"] = native_handle;
return true;
}
}
class TeamSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeamSpeakIdentity;
constructor(connection: ServerConnection, identity: TeamSpeakIdentity) {
super(connection);
this.identity = identity;
}
start_handshake() {
this.connection.commandHandler["handshakeidentityproof"] = this.handle_proof.bind(this);
this.connection.sendCommand("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
publicKey: this.identity.publicKey()
}).catch(error => {
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");
});
}
private handle_proof(json) {
const proof = this.identity.signMessage(json[0]["message"]);
this.connection.sendCommand("handshakeindentityproof", {proof: proof}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute proof (" + error + ")");
}).then(() => this.trigger_success());
}
}
export class TeamSpeakIdentity implements Identity {
private handle: any;
private _name: string;
constructor(handle: any, name: string) {
this.handle = handle;
this._name = name;
}
securityLevel() : number | undefined {
return parseInt(TSIdentityHelper.unwarpString(TSIdentityHelper.funcationCalculateSecurityLevel(this.handle)));
}
name() : string { return this._name; }
uid() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.functionUid(this.handle));
}
type() : IdentitifyType { return IdentitifyType.TEAMSPEAK; }
signMessage(message: string): string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationSignMessage(this.handle, message, message.length));
}
exported() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationExportIdentity(this.handle));
}
publicKey() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationPublicKey(this.handle));
}
valid() : boolean { return this.handle !== undefined; }
decode(data) : boolean {
data = JSON.parse(data);
if(data.version != 1) return false;
if(!TSIdentityHelper.load_identity(this, data["key"]))
return false;
this._name = data["name"];
return true;
}
encode?() : string {
if(!this.handle) return undefined;
const key = this.exported();
if(!key) return undefined;
return JSON.stringify({
key: key,
name: this._name,
version: 1
})
}
spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler {
return new TeamSpeakHandshakeHandler(connection, this);
}
}
export function setup_teamspeak() : boolean {
return TSIdentityHelper.setup();
}
}

View File

@ -69,13 +69,16 @@ class ControlBar {
{
let bookmark = this.htmlTag.find(".btn_bookmark");
bookmark.find(".button-dropdown").on('click', () => {
bookmark.find(".dropdown").addClass("displayed");
bookmark.find("> .dropdown").addClass("displayed");
});
bookmark.on('mouseleave', () => {
bookmark.find(".dropdown").removeClass("displayed");
bookmark.find("> .dropdown").removeClass("displayed");
});
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
this.update_bookmarks()
this.update_bookmarks();
this.update_bookmark_status();
}
{
let query = this.htmlTag.find(".btn_query");
@ -304,28 +307,99 @@ class ControlBar {
}
}
private on_bookmark_server_add() {
if(globalClient && globalClient.connected) {
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => true, result => {
if(result) {
const bookmark = bookmarks.create_bookmark(result as string, bookmarks.bookmarks(), {
server_port: globalClient.serverConnection._remote_address.port,
server_address: globalClient.serverConnection._remote_address.host,
server_password: "",
server_password_hash: ""
}, globalClient.getClient().clientNickName());
bookmarks.save_bookmark(bookmark);
this.update_bookmarks()
}
}).open();
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
}
update_bookmark_status() {
this.htmlTag.find(".btn_bookmark_add").removeClass("hidden").addClass("disabled");
this.htmlTag.find(".btn_bookmark_remove").addClass("hidden");
}
update_bookmarks() {
//<div class="btn_bookmark_connect" target="localhost"><a>Localhost</a></div>
let tag_bookmark = this.htmlTag.find(".btn_bookmark .dropdown");
tag_bookmark.find(".bookmark, .bookmark_directory").detach();
tag_bookmark.find(".bookmark, .directory").detach();
for(const bookmark of bookmarks.bookmarks().content) {
const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => {
if(bookmark.type == bookmarks.BookmarkType.ENTRY) {
tag_bookmark.append(
$.spawn("div")
const mark = <bookmarks.Bookmark>bookmark;
return $.spawn("div")
.addClass("bookmark")
/* /.attr("bookmark-uuid", bookmark.unique_id) */
.append(
$.spawn("div").addClass("icon client-server")
)
.append(
$.spawn("div")
.addClass("name")
.text(bookmark.display_name)
.on('click', event => {
spawnConnectModal()
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed");
this.handle.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profiles.find_profile(mark.connect_profile) || profiles.default_profile(),
mark.nickname,
{
password: mark.server_properties.server_password_hash,
hashed: true
}
);
})
)
} else {
const mark = <bookmarks.DirectoryBookmark>bookmark;
const container = $.spawn("div").addClass("sub-menu dropdown");
for(const member of mark.content)
container.append(build_entry(member));
return $.spawn("div")
.addClass("directory")
.append(
$.spawn("div").addClass("icon client-folder")
)
.append(
$.spawn("div")
.addClass("name")
.text(bookmark.display_name)
)
.append(
$.spawn("div").addClass("arrow right")
)
.append(
$.spawn("div").addClass("sub-container")
.append(container)
)
}
//TODO add bookmark directories here
};
for(const bookmark of bookmarks.bookmarks().content) {
const entry = build_entry(bookmark);
tag_bookmark.append(entry);
}
}
private on_bookmark_manage() {
Modals.spawnBookmarkModal();
}
get query_visibility() {
return this._query_visible;
}

View File

@ -0,0 +1,258 @@
/// <reference path="../../utils/modal.ts" />
/// <reference path="../../proto.ts" />
namespace Modals {
function bookmark_tag(callback_select: (entry, tag) => any, bookmark: bookmarks.Bookmark | bookmarks.DirectoryBookmark) {
const tag = $("#tmpl_manage_bookmarks-list_entry").renderTag({
name: bookmark.display_name,
type: bookmark.type == bookmarks.BookmarkType.DIRECTORY ? "directory" : "bookmark"
});
tag.find(".name").on('click', () => {
callback_select(bookmark, tag);
tag.addClass("selected");
});
if(bookmark.type == bookmarks.BookmarkType.DIRECTORY) {
const casted = <bookmarks.DirectoryBookmark>bookmark;
for(const member of casted.content)
tag.find("> .members").append(bookmark_tag(callback_select, member));
}
return tag;
}
function parent_tag(select_tag: JQuery, prefix: string, bookmark: bookmarks.Bookmark | bookmarks.DirectoryBookmark) {
if(bookmark.type == bookmarks.BookmarkType.DIRECTORY) {
const casted = <bookmarks.DirectoryBookmark>bookmark;
select_tag.append(
$.spawn("option")
.val(casted.unique_id)
.text(prefix + casted.display_name)
);
for(const member of casted.content)
parent_tag(select_tag, prefix + " ", member);
}
}
export function spawnBookmarkModal() {
let modal: Modal;
modal = createModal({
header: tr("Manage bookmarks"),
body: () => {
let template = $("#tmpl_manage_bookmarks").renderTag({ });
template = $.spawn("div").append(template);
let selected_bookmark: bookmarks.Bookmark | bookmarks.DirectoryBookmark | undefined;
let update_name: () => any;
const update_bookmarks = () => { //list bookmarks
template.find(".list").empty();
const callback_selected = (entry: bookmarks.Bookmark | bookmarks.DirectoryBookmark, tag: JQuery) => {
template.find(".selected").removeClass("selected");
if(selected_bookmark == entry) return;
selected_bookmark = entry;
update_name = () => tag.find("> .name").text(entry.display_name);
template.find(".bookmark-setting").hide();
template.find(".setting-bookmark-name").val(selected_bookmark.display_name);
if(selected_bookmark.type == bookmarks.BookmarkType.ENTRY) {
template.find(".bookmark-setting-bookmark").show();
const casted = <bookmarks.Bookmark>selected_bookmark;
const profile = profiles.find_profile(casted.connect_profile) || profiles.default_profile();
template.find(".setting-bookmark-profile").val(profile.id);
template.find(".setting-server-host").val(casted.server_properties.server_address);
template.find(".setting-server-port").val(casted.server_properties.server_port);
template.find(".setting-server-password").val(casted.server_properties.server_password_hash || casted.server_properties.server_password);
template.find(".setting-username").val(casted.nickname);
template.find(".setting-channel").val(casted.default_channel);
template.find(".setting-channel-password").val(casted.default_channel_password_hash || casted.default_channel_password);
} else {
template.find(".bookmark-setting-directory").show();
}
};
for(const bookmark of bookmarks.bookmarks().content) {
template.find(".list").append(bookmark_tag(callback_selected, bookmark));
}
console.log( template.find(".list").find(".bookmark, .directory"));
template.find(".list").find(".bookmark, .directory").eq(0).find("> .name").trigger('click');
};
{ //General buttons
template.find(".button-create").on('click', event => {
let create_modal: Modal;
create_modal = createModal({
header: tr("Create a new entry"),
body: () => {
let template = $("#tmpl_manage_bookmarks-create").renderTag({ });
template = $.spawn("div").append(template);
for(const bookmark of bookmarks.bookmarks().content)
parent_tag(template.find(".bookmark-parent"), "", bookmark);
if(selected_bookmark) {
const parent = selected_bookmark.type == bookmarks.BookmarkType.ENTRY ?
bookmarks.parent_bookmark(selected_bookmark as bookmarks.Bookmark) :
selected_bookmark;
if(parent)
template.find(".bookmark-parent").val(parent.unique_id);
}
template.find(".bookmark-name").on('change, keyup', event => {
template.find(".button-create").prop("disabled", (<HTMLInputElement>event.target).value.length < 3);
});
template.find(".button-create").prop("disabled", true).on('click', event => {
const name = template.find(".bookmark-name").val() as string;
const parent_uuid = template.find(".bookmark-parent").val() as string;
const parent = bookmarks.find_bookmark(parent_uuid);
let bookmark;
if(template.find(".bookmark-type").val() == "directory") {
bookmark = bookmarks.create_bookmark_directory(parent as bookmarks.DirectoryBookmark || bookmarks.bookmarks(), name);
} else {
bookmark = bookmarks.create_bookmark(name, parent as bookmarks.DirectoryBookmark || bookmarks.bookmarks(), {
server_port: 9987,
server_address: "ts.teaspeak.de"
}, "Another TeaSpeak user");
}
bookmarks.save_bookmark(bookmark);
create_modal.close();
update_bookmarks();
});
return template;
},
footer: 400
});
create_modal.open();
});
template.find(".button-delete").on('click', event => {
if(!selected_bookmark) return;
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this entry?"), result => {
if(result) {
bookmarks.delete_bookmark(selected_bookmark);
bookmarks.save_bookmark(selected_bookmark); /* save the deleted state */
update_bookmarks();
}
});
});
/* bookmark listener */
{
template.find(".setting-bookmark-profile").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.connect_profile = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-server-host").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.server_properties.server_address = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-server-port").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.server_properties.server_port = parseInt(element.value);
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-server-password").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.server_properties.server_password = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-username").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.nickname = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-channel").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.default_channel = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
template.find(".setting-channel-password").on('change', event => {
if(!selected_bookmark || selected_bookmark.type != bookmarks.BookmarkType.ENTRY) return;
const casted = <bookmarks.Bookmark>selected_bookmark;
const element = <HTMLInputElement>event.target;
casted.default_channel_password = element.value;
bookmarks.save_bookmark(selected_bookmark);
});
}
/* listener for both */
{
template.find(".setting-bookmark-name").on('change', event => {
if(!selected_bookmark) return;
const element = <HTMLInputElement>event.target;
if(element.value.length >= 3) {
selected_bookmark.display_name = element.value;
bookmarks.save_bookmark(selected_bookmark);
if(update_name)
update_name();
}
});
}
}
/* connect profile initialisation */
{
const list = template.find(".setting-bookmark-profile");
for(const profile of profiles.profiles()) {
const tag = $.spawn("option").val(profile.id).text(profile.profile_name);
if(profile.id == "default")
tag.css("font-weight", "bold");
list.append(tag);
}
}
update_bookmarks();
template.find(".button-close").on('click', event => modal.close());
return template;
},
footer: undefined,
width: 750
});
modal.close_listener.push(() => globalClient.controlBar.update_bookmarks());
modal.open();
}
}

View File

@ -1,8 +1,8 @@
/// <reference path="../../utils/modal.ts" />
namespace Modals {
export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, def_connect_type?: { identity: IdentitifyType, enforce: boolean}) {
let connectIdentity: Identity;
export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) {
let selected_profile: profiles.ConnectionProfile;
const connectModal = createModal({
header: function() {
let header = $.spawn("div");
@ -12,13 +12,13 @@ namespace Modals {
body: function () {
let tag = $("#tmpl_connect").renderTag({
client: native_client,
forum_path: settings.static("forum_path"),
forum_valid: !!forumIdentity
forum_path: settings.static("forum_path")
});
let updateFields = function () {
if(connectIdentity) tag.find(".connect_nickname").attr("placeholder", connectIdentity.name());
else tag.find(".connect_nickname").attr("");
if(selected_profile) tag.find(".connect_nickname").attr("placeholder", selected_profile.default_username);
else
tag.find(".connect_nickname").attr("");
let button = tag.parents(".modal-content").find(".connect_connect_button");
@ -30,7 +30,7 @@ namespace Modals {
let field_nickname = tag.find(".connect_nickname");
let nickname = field_nickname.val().toString();
settings.changeGlobal("connect_name", nickname);
let flag_nickname = (nickname.length == 0 && connectIdentity && connectIdentity.name().length > 0) || nickname.length >= 3 && nickname.length <= 32;
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
if(flag_address) {
if(field_address.hasClass("invalid_input"))
@ -48,7 +48,7 @@ namespace Modals {
field_nickname.addClass("invalid_input");
}
if(!flag_nickname || !flag_address || !connectIdentity) {
if(!flag_nickname || !flag_address || !selected_profile || !selected_profile.valid()) {
button.prop("disabled", true);
} else {
button.prop("disabled", false);
@ -63,108 +63,37 @@ namespace Modals {
if(event.keyCode == JQuery.Key.Enter && !event.shiftKey)
tag.parents(".modal-content").find(".connect_connect_button").trigger('click');
});
tag.find(".connect_nickname").on("keyup", () => updateFields());
tag.find(".identity_select").on('change', function (this: HTMLSelectElement) {
settings.changeGlobal("connect_identity_type", IdentitifyType[this.value]);
tag.find(".error_message").hide();
tag.find(".identity_config:not(" + ".identity_config_" + this.value + ")").hide();
tag.find(".identity_config_" + this.value).show().trigger('shown');
});
tag.find(".identity_select").val(IdentitifyType[def_connect_type && def_connect_type.enforce ? def_connect_type.identity : settings.global("connect_identity_type", (def_connect_type || { identity: undefined }).identity || IdentitifyType.TEAFORO)]);
setTimeout(() => tag.find(".identity_select").trigger('change'), 0); //For some reason could not be run instantly
{
tag.find(".identity_file").change(function (this: HTMLInputElement) {
const reader = new FileReader();
reader.onload = function() {
connectIdentity = TSIdentityHelper.loadIdentityFromFileContains(reader.result as string);
console.log(connectIdentity.uid());
if(!connectIdentity) tag.find(".error_message").text(tr("Could not read identity! ") + TSIdentityHelper.last_error());
else {
tag.find(".identity_string").val((connectIdentity as TeamSpeakIdentity).exported());
settings.changeGlobal("connect_identity_teamspeak_identity", (connectIdentity as TeamSpeakIdentity).exported());
}
(!!connectIdentity ? tag.hide : tag.show).apply(tag.find(".error_message"));
updateFields();
};
reader.onerror = ev => {
tag.find(".error_message").text(tr("Could not read identity file!")).show();
updateFields();
};
reader.readAsText(this.files[0]);
});
tag.find(".identity_string").on('change', function (this: HTMLInputElement) {
if(this.value.length == 0){
tag.find(".error_message").text(tr("Please select an identity!"));
connectIdentity = undefined;
} else {
connectIdentity = TSIdentityHelper.loadIdentity(this.value);
if(!connectIdentity) tag.find(".error_message").text("Could not parse identity! " + TSIdentityHelper.last_error());
else settings.changeGlobal("connect_identity_teamspeak_identity", this.value);
}
(!!connectIdentity ? tag.hide : tag.show).apply(tag.find(".error_message"));
tag.find(".identity_file").val("");
updateFields();
});
tag.find(".identity_string").val(settings.global("connect_identity_teamspeak_identity", ""));
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAMSPEAK]).on('shown', ev => {
tag.find(".identity_string").trigger('change');
});
}
{
const element = tag.find(".identity_config_" + IdentitifyType[IdentitifyType.TEAFORO]);
element.on('shown', ev => {
console.log(tr("Updating via shown"));
connectIdentity = forumIdentity;
if(connectIdentity) {
element.find(".connected").show();
element.find(".disconnected").hide();
} else {
element.find(".connected").hide();
element.find(".disconnected").show();
}
updateFields();
});
if(native_client) {
tag.find(".native-teaforo-login").on('click', event => {
tag.find(".button-manage-profiles").on('click', event => {
const modal = Modals.spawnSettingsModal();
setTimeout(() => {
const forum = require("teaforo.js");
const call = () => {
try {
console.log("Trigger update!");
element.trigger('shown');
} catch ($) { console.log($); }
if(connectModal.shown)
forum.register_callback(call);
};
forum.register_callback(call);
forum.open();
}, 0);
modal.htmlTag.find(".tab-profiles").parent(".entry").trigger('click');
}, 100);
return true;
});
}
}
{
tag.find(".identity_config_" + IdentitifyType[IdentitifyType.NICKNAME]).on('shown', ev => {
connectIdentity = new NameIdentity(tag.find(".connect_nickname").val() as string);
const select_tag = tag.find(".profile-select-container select");
const select_invalid_tag = tag.find(".profile-invalid");
for(const profile of profiles.profiles()) {
select_tag.append(
$.spawn("option").text(profile.profile_name).val(profile.id)
);
}
select_tag.on('change', event => {
selected_profile = profiles.find_profile(select_tag.val() as string);
if(!selected_profile || !selected_profile.valid())
select_invalid_tag.show();
else
select_invalid_tag.hide();
updateFields();
});
tag.find(".connect_nickname").on("keyup", () => {
if(connectIdentity instanceof NameIdentity)
connectIdentity.set_name(tag.find(".connect_nickname").val() as string);
});
select_tag.val('default').trigger('change');
}
if(!settings.static("localhost_debug", false)) {
tag.find(".identity_select option[value=" + IdentitifyType[IdentitifyType.NICKNAME] + "]").remove();
}
}
tag.find(".connect_nickname").on("keyup", () => updateFields());
//connect_address
return tag;
@ -186,8 +115,8 @@ namespace Modals {
let address = field_address.val().toString();
globalClient.startConnection(
address,
connectIdentity,
tag.parents(".modal-content").find(".connect_nickname").val().toString(),
selected_profile,
tag.parents(".modal-content").find(".connect_nickname").val().toString() || selected_profile.default_username,
{password: tag.parents(".modal-content").find(".connect_password").val().toString(), hashed: false}
);
});

View File

@ -4,18 +4,26 @@
/// <reference path="../../voice/AudioController.ts" />
namespace Modals {
import info = log.info;
import TranslationRepository = i18n.TranslationRepository;
import ConnectionProfile = profiles.ConnectionProfile;
import IdentitifyType = profiles.identities.IdentitifyType;
export function spawnSettingsModal() {
export function spawnSettingsModal() : Modal{
let modal;
modal = createModal({
header: tr("Settings"),
body: () => {
let template = $("#tmpl_settings").renderTag();
let template = $("#tmpl_settings").renderTag({
client: native_client,
valid_forum_identity: profiles.identities.valid_static_forum_identity(),
forum_path: settings.static("forum_path"),
});
template = $.spawn("div").append(template);
initialiseSettingListeners(modal,template = template.tabify());
initialise_translations(template.find(".settings-translations"));
initialise_profiles(modal, template.find(".settings-profiles"));
return template;
},
footer: () => {
@ -35,6 +43,7 @@ namespace Modals {
width: 750
});
modal.open();
return modal;
}
function initialiseSettingListeners(modal: Modal, tag: JQuery) {
@ -443,4 +452,250 @@ namespace Modals {
update_list();
}
}
function initialise_profiles(modal: Modal, tag: JQuery) {
const settings_tag = tag.find(".profile-settings");
let selected_profile: ConnectionProfile;
let nickname_listener: () => any;
let status_listener: () => any;
const display_settings = (profile: ConnectionProfile) => {
selected_profile = profile;
settings_tag.find(".setting-name").val(profile.profile_name);
settings_tag.find(".setting-default-nickname").val(profile.default_username);
settings_tag.find(".setting-default-password").val(profile.default_password);
{
//change listener
const select_tag = settings_tag.find(".select-container select")[0] as HTMLSelectElement;
const type = profile.selected_identity_type.toLowerCase();
select_tag.onchange = () => {
console.log("Selected: " + select_tag.value);
settings_tag.find(".identity-settings.active").removeClass("active");
settings_tag.find(".identity-settings-" + select_tag.value).addClass("active");
profile.selected_identity_type = select_tag.value.toLowerCase();
const selected_type = profile.selected_type();
const identity = profile.selected_identity();
profiles.mark_need_save();
if(selected_type == IdentitifyType.TEAFORO) {
const forum_tag = settings_tag.find(".identity-settings-teaforo");
forum_tag.find(".connected .disconnected").hide();
if(identity && identity.valid()) {
forum_tag.find(".connected").show();
} else {
forum_tag.find(".disconnected").show();
}
} else if(selected_type == IdentitifyType.TEAMSPEAK) {
console.log("Set: " + identity);
const teamspeak_tag = settings_tag.find(".identity-settings-teamspeak");
if(identity)
teamspeak_tag.find(".identity_string").val((identity as profiles.identities.TeamSpeakIdentity).exported());
else
teamspeak_tag.find(".identity_string").val("");
} else if(selected_type == IdentitifyType.NICKNAME) {
const name_tag = settings_tag.find(".identity-settings-nickname");
if(identity)
name_tag.find("input").val(identity.name());
else
name_tag.find("input").val("");
}
};
select_tag.value = type;
select_tag.onchange(undefined);
}
};
const update_profile_list = () => {
const profile_list = tag.find(".profile-list .list").empty();
const profile_template = $("#settings-profile-list-entry");
for(const profile of profiles.profiles()) {
const list_tag = profile_template.renderTag({
profile_name: profile.profile_name,
id: profile.id
});
const profile_status_update = () => {
list_tag.find(".status").hide();
if(profile.valid())
list_tag.find(".status-valid").show();
else
list_tag.find(".status-invalid").show();
};
list_tag.on('click', event => {
/* update ui */
profile_list.find(".selected").removeClass("selected");
list_tag.addClass("selected");
if(profile == selected_profile) return;
nickname_listener = () => list_tag.find(".name").text(profile.profile_name);
status_listener = profile_status_update;
display_settings(profile);
});
profile_list.append(list_tag);
if((!selected_profile && profile.id == "default") || selected_profile == profile)
setTimeout(() => list_tag.trigger('click'), 1);
profile_status_update();
}
};
/* identity settings */
{
{ //TeamSpeak change listener
const teamspeak_tag = settings_tag.find(".identity-settings-teamspeak");
const display_error = (error?: string) => {
if(error){
teamspeak_tag.find(".error-message").show().html(error);
} else
teamspeak_tag.find(".error-message").hide();
status_listener();
};
teamspeak_tag.find(".identity_file").on('change', event => {
if(!selected_profile) return;
const element = event.target as HTMLInputElement;
const file_reader = new FileReader();
file_reader.onload = function() {
const identity = profiles.identities.TSIdentityHelper.loadIdentityFromFileContains(file_reader.result as string);
if(!identity) {
display_error(tr("Failed to parse identity.<br>Reason: ") + profiles.identities.TSIdentityHelper.last_error());
return;
} else {
teamspeak_tag.find(".identity_string").val(identity.exported());
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
profiles.mark_need_save();
display_error(undefined);
}
};
file_reader.onerror = ev => {
console.error(tr("Failed to read give identity file: %o"), ev);
display_error(tr("Failed to read file!"));
return;
};
if(element.files && element.files.length > 0)
file_reader.readAsText(element.files[0]);
});
teamspeak_tag.find(".identity_string").on('change', event => {
if(!selected_profile) return;
const element = event.target as HTMLInputElement;
if(element.value.length == 0) {
display_error("Please provide an identity");
} else {
const identity = profiles.identities.TSIdentityHelper.loadIdentity(element.value);
if(!identity) {
display_error("Failed to parse identity string!");
return;
}
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
profiles.mark_need_save();
display_error(undefined);
}
});
}
{ //The forum
const teaforo_tag = settings_tag.find(".identity-settings-teaforo");
if(native_client) {
teaforo_tag.find(".native-teaforo-login").on('click', event => {
setTimeout(() => {
const forum = require("teaforo.js");
const call = () => {
if(modal.shown) {
display_settings(selected_profile);
status_listener();
}
};
forum.register_callback(call);
forum.open();
}, 0);
});
}
}
//TODO add the name!
}
/* general settings */
{
settings_tag.find(".setting-name").on('change', event => {
const value = settings_tag.find(".setting-name").val() as string;
if(value && selected_profile) {
selected_profile.profile_name = value;
if(nickname_listener)
nickname_listener();
profiles.mark_need_save();
status_listener();
}
});
settings_tag.find(".setting-default-nickname").on('change', event => {
const value = settings_tag.find(".setting-default-nickname").val() as string;
if(value && selected_profile) {
selected_profile.default_username = value;
profiles.mark_need_save();
status_listener();
}
});
settings_tag.find(".setting-default-password").on('change', event => {
const value = settings_tag.find(".setting-default-password").val() as string;
if(value && selected_profile) {
selected_profile.default_username = value;
profiles.mark_need_save();
status_listener();
}
});
}
/* general buttons */
{
tag.find(".button-add-profile").on('click', event => {
createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:<br>"), text => text.length > 0 && !profiles.find_profile_by_name(text), value => {
if(value) {
display_settings(profiles.create_new_profile(value as string));
update_profile_list();
profiles.mark_need_save();
}
}).open();
});
tag.find(".button-set-default").on('click', event => {
if(selected_profile && selected_profile.id != 'default') {
profiles.set_default_profile(selected_profile);
update_profile_list();
profiles.mark_need_save();
}
});
tag.find(".button-delete").on('click', event => {
if(selected_profile && selected_profile.id != 'default') {
event.preventDefault();
spawnYesNo(tr("Are you sure?"), tr ("Do you really want to delete this profile?"), result => {
if(result) {
profiles.delete_profile(selected_profile);
update_profile_list();
}
});
}
});
}
modal.close_listener.push(() => {
if(profiles.requires_save())
profiles.save();
});
update_profile_list();
}
}

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit 0221bd137ef5bbc846018ff86deda0aca38aed26
Subproject commit 86dae7fae51db65d63febf4c3e15b8dc629a5732