Merged with master

canary
WolverinDEV 2020-04-06 16:32:33 +02:00
commit cccd780724
86 changed files with 121759 additions and 39521 deletions

5
.gitignore vendored
View File

@ -15,6 +15,7 @@ node_modules/
# Some build output # Some build output
/dist/ /dist/
/declarations/ /declarations/
/travis-build/
# Don't add the created packages to git # Don't add the created packages to git
/TeaSpeakUI.tar.gz /TeaSpeakUI.tar.gz
@ -28,4 +29,6 @@ node_modules/
/*.js.map /*.js.map
/webpack/*.js /webpack/*.js
/webpack/*.js.map /webpack/*.js.map
/files_*.pem

7
.gitmodules vendored
View File

@ -1,7 +1,6 @@
[submodule "asm/libraries/opus"]
path = asm/libraries/opus
url = https://github.com/xiph/opus.git
branch = 1.1.2
[submodule "vendor/xbbcode"] [submodule "vendor/xbbcode"]
path = vendor/xbbcode path = vendor/xbbcode
url = https://github.com/WolverinDEV/XBBCode.git url = https://github.com/WolverinDEV/XBBCode.git
[submodule "web/native-codec/libraries/opus"]
path = web/native-codec/libraries/opus
url = https://github.com/xiph/opus.git

View File

@ -8,15 +8,19 @@ node_js:
services: services:
- docker - docker
sudo: required
before_install: before_install:
# If ever run on windows make sure you don't run this in the git bash! # If ever run on windows make sure you don't run this in the git bash!
- docker run -dit --name emscripten -v "$(pwd)":"/src/" trzeci/emscripten:sdk-incoming-64bit bash - docker run -dit --name emscripten -v "$(pwd)":"/src/" trzeci/emscripten:sdk-incoming-64bit bash
- chmod +x ./scripts/travis.sh
- chmod +x ./scripts/travis_deploy.sh
jobs: branches:
include: only:
- stage: "build" - master
name: TeaWeb build master branch - develop
script:
- "./scripts/travis.sh --enable-release --enable-debug || travis_terminate 1;" script:
- "./scripts/travis_deploy.sh || travis_terminate 1;" - "./scripts/travis.sh --enable-release --enable-debug || travis_terminate 1;"
if: branch = master - "./scripts/travis_deploy.sh || travis_terminate 1;"

View File

@ -1,4 +1,14 @@
# Changelog: # Changelog:
* **04.03.20**
- Implemented the new music bot playlist song list
- Implemented the missing server log message builders
* **03.03.20**
- Using webpack instead of our own loaded (a lot of restructuring)
- Fixed that the microphone slider hasn't worked for touch devices
- Fixed a bug which caused that audio data hasn't been transmitted
- Added the ability to start a https web server
* **28.03.20** * **28.03.20**
- Fixed a bug within the permission editor which kicks you from the server - Fixed a bug within the permission editor which kicks you from the server

@ -1 +0,0 @@
Subproject commit 655cc54c564b84ef2827f0b2152ce3811046201e

11
auto-build/logs/build.log Normal file
View File

@ -0,0 +1,11 @@
[EXECUTE] Executing commands:
[EXECUTE] git pull
[EXECUTE] git submodule update --init --recursive --remote --checkout
[EXECUTE] git status &>/dev/null
$> git pull
$> git submodule update --init --recursive --remote --checkout
$> git status &>/dev/null
[EXECUTE] Command exited with exit code 0 (Runtime 1657ms)
[EXECUTE] Executing command "npm install"
$> npm install
[EXECUTE] Command exited with exit code 2 (Runtime 26ms)

View File

@ -15307,8 +15307,10 @@ class ConnectionHandler {
} }
} }
} }
if (control_bar.current_connection_handler() === this) if (control_bar.current_connection_handler() === this) {
control_bar.apply_server_voice_state(); control_bar.apply_server_voice_state();
top_menu.update_state(); //TODO: Only run "small" update?
}
} }
sync_status_with_server() { sync_status_with_server() {
if (this.serverConnection.connected()) if (this.serverConnection.connected())

165
file.ts
View File

@ -9,6 +9,7 @@ import * as mt from "mime-types";
import * as os from "os"; import * as os from "os";
import {PathLike} from "fs"; import {PathLike} from "fs";
import {ChildProcess} from "child_process"; import {ChildProcess} from "child_process";
import * as https from "https";
/* All project files */ /* All project files */
type ProjectResourceType = "html" | "js" | "css" | "wasm" | "wav" | "json" | "img" | "i18n" | "pem"; type ProjectResourceType = "html" | "js" | "css" | "wasm" | "wav" | "json" | "img" | "i18n" | "pem";
@ -160,7 +161,7 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
"build-target": "dev|rel", "build-target": "dev|rel",
"path": "wasm/", "path": "wasm/",
"local-path": "./asm/generated/" "local-path": "./web/native-codec/generated/"
}, },
{ /* web css files */ { /* web css files */
"web-only": true, "web-only": true,
@ -342,147 +343,6 @@ const WEB_APP_FILE_LIST = [
...CERTACCEPT_FILE_LIST, ...CERTACCEPT_FILE_LIST,
]; ];
//const WEB_APP_FILE_LIST = [
// ...APP_FILE_LIST_SHARED_VENDORS,
// { /* shared html and php files */
// "type": "html",
// "search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
// "build-target": "dev|rel",
//
// "path": "./",
// "local-path": "./shared/html/"
// },
// { /* javascript files as manifest.json */
// "type": "js",
// "search-pattern": /.*$/,
// "build-target": "dev|rel",
//
// "path": "js/",
// "local-path": "./dist/"
// },
// { /* loader javascript file */
// "type": "js",
// "search-pattern": /.*$/,
// "build-target": "dev|rel",
//
// "path": "js/",
// "local-path": "./loader/dist/"
// },
// { /* shared developer single css files */
// "type": "css",
// "search-pattern": /.*\.css$/,
// "build-target": "dev",
//
// "path": "css/",
// "local-path": "./shared/css/"
// },
// { /* shared css mapping files (development mode only) */
// "type": "css",
// "search-pattern": /.*\.(css.map|scss)$/,
// "build-target": "dev",
//
// "path": "css/",
// "local-path": "./shared/css/",
// "req-parm": ["--mappings"]
// },
// { /* shared release css files */
// "type": "css",
// "search-pattern": /.*\.css$/,
// "build-target": "rel",
//
// "path": "css/",
// "local-path": "./shared/generated/"
// },
// { /* shared release css files */
// "type": "css",
// "search-pattern": /.*\.css$/,
// "build-target": "rel",
//
// "path": "css/loader/",
// "local-path": "./shared/css/loader/"
// },
// { /* shared release css files */
// "type": "css",
// "search-pattern": /.*\.css$/,
// "build-target": "dev|rel",
//
// "path": "css/theme/",
// "local-path": "./shared/css/theme/"
// },
// { /* shared sound files */
// "type": "wav",
// "search-pattern": /.*\.wav$/,
// "build-target": "dev|rel",
//
// "path": "audio/",
// "local-path": "./shared/audio/"
// },
// { /* shared data sound files */
// "type": "json",
// "search-pattern": /.*\.json/,
// "build-target": "dev|rel",
//
// "path": "audio/",
// "local-path": "./shared/audio/"
// },
// { /* shared image files */
// "type": "img",
// "search-pattern": /.*\.(svg|png)/,
// "build-target": "dev|rel",
//
// "path": "img/",
// "local-path": "./shared/img/"
// },
// { /* own webassembly files */
// "type": "wasm",
// "search-pattern": /.*\.(wasm)/,
// "build-target": "dev|rel",
//
// "path": "wat/",
// "local-path": "./shared/wat/"
// },
//
//
// /* web specific */
// { /* generated assembly files */
// "web-only": true,
// "type": "wasm",
// "search-pattern": /.*\.(wasm)/,
// "build-target": "dev|rel",
//
// "path": "wasm/",
// "local-path": "./asm/generated/"
// },
// { /* web css files */
// "web-only": true,
// "type": "css",
// "search-pattern": /.*\.css$/,
// "build-target": "dev|rel",
//
// "path": "css/",
// "local-path": "./web/css/"
// },
// { /* web html files */
// "web-only": true,
// "type": "html",
// "search-pattern": /.*\.(php|html)/,
// "build-target": "dev|rel",
//
// "path": "./",
// "local-path": "./web/html/"
// },
// { /* translations */
// "web-only": true, /* Only required for the web client */
// "type": "i18n",
// "search-pattern": /.*\.(translation|json)/,
// "build-target": "dev|rel",
//
// "path": "i18n/",
// "local-path": "./shared/i18n/"
// }
//] as any;
//@ts-ignore //@ts-ignore
declare module "fs-extra" { declare module "fs-extra" {
export function exists(path: PathLike): Promise<boolean>; export function exists(path: PathLike): Promise<boolean>;
@ -636,6 +496,8 @@ namespace server {
let server: http.Server; let server: http.Server;
let php: string; let php: string;
let options: Options; let options: Options;
const use_https = false;
export async function launch(_files: ProjectResource[], options_: Options) { export async function launch(_files: ProjectResource[], options_: Options) {
options = options_; options = options_;
files = _files; files = _files;
@ -654,7 +516,24 @@ namespace server {
console.error("failed to validate php interpreter: %o", error); console.error("failed to validate php interpreter: %o", error);
throw "invalid php interpreter"; throw "invalid php interpreter";
} }
server = http.createServer(handle_request);
if(process.env["ssl_enabled"] || use_https) {
//openssl req -nodes -new -x509 -keyout files_key.pem -out files_cert.pem
const key_file = process.env["ssl_key"] || path.join(__dirname, "files_key.pem");
const cert_file = process.env["ssl_cert"] || path.join(__dirname, "files_cert.pem");
if(!await fs.pathExists(key_file))
throw "Missing ssl key file";
if(!await fs.pathExists(cert_file))
throw "Missing ssl cert file";
server = https.createServer({
key: await fs.readFile(key_file),
cert: await fs.readFile(cert_file),
}, handle_request);
} else {
server = http.createServer(handle_request);
}
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
server.on('error', reject); server.on('error', reject);
server.listen(options.port, () => { server.listen(options.port, () => {

984
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"main": "main.js", "main": "main.js",
"directories": {}, "directories": {},
"scripts": { "scripts": {
"compile-sass": "sass --update shared/css/:shared/css/ web/css/:web/css/ client/css/:client/css/", "compile-sass": "sass --update shared/css/:shared/css/ web/css/:web/css/ client/css/:client/css/ vendor/:vendor/",
"compile-project-base": "tsc -p tsbaseconfig.json", "compile-project-base": "tsc -p tsbaseconfig.json",
"dtsgen": "node tools/dtsgen/index.js", "dtsgen": "node tools/dtsgen/index.js",
"trgen": "node tools/trgen/index.js", "trgen": "node tools/trgen/index.js",
@ -17,11 +17,13 @@
"build-web": "webpack --config webpack-web.config.js", "build-web": "webpack --config webpack-web.config.js",
"watch-web": "webpack --watch --config webpack-web.config.js", "watch-web": "webpack --watch --config webpack-web.config.js",
"build-client": "webpack --config webpack-client.config.js", "build-client": "webpack --config webpack-client.config.js",
"watch-client": "webpack --watch --config webpack-client.config.js" "watch-client": "webpack --watch --config webpack-client.config.js",
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
}, },
"author": "TeaSpeak (WolverinDEV)", "author": "TeaSpeak (WolverinDEV)",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@google-cloud/translate": "^5.3.0",
"@types/dompurify": "^2.0.1", "@types/dompurify": "^2.0.1",
"@types/emscripten": "^1.38.0", "@types/emscripten": "^1.38.0",
"@types/fs-extra": "^8.0.1", "@types/fs-extra": "^8.0.1",
@ -55,7 +57,7 @@
"terser": "^4.2.1", "terser": "^4.2.1",
"terser-webpack-plugin": "latest", "terser-webpack-plugin": "latest",
"ts-loader": "^6.2.2", "ts-loader": "^6.2.2",
"typescript": "3.6.5", "typescript": "^3.7.0",
"wabt": "^1.0.13", "wabt": "^1.0.13",
"webpack": "^4.42.1", "webpack": "^4.42.1",
"webpack-bundle-analyzer": "^3.6.1", "webpack-bundle-analyzer": "^3.6.1",

View File

@ -42,6 +42,7 @@ if [[ $_exit_code -ne 0 ]]; then
fi fi
echo "Generating required build tooks" echo "Generating required build tooks"
chmod +x ./tools/build_trgen.sh
./tools/build_trgen.sh; _exit_code=$? ./tools/build_trgen.sh; _exit_code=$?
if [[ $_exit_code -ne 0 ]]; then if [[ $_exit_code -ne 0 ]]; then
echo "Failed to build build_typescript translation generator" echo "Failed to build build_typescript translation generator"
@ -56,14 +57,22 @@ if [[ $_exit_code -ne 0 ]]; then
fi fi
echo "Compile vendor XBBCode" echo "Compile vendor XBBCode"
execute_ttsc -p ./vendor/xbbcode/tsconfig.json; _exit_code=$? execute_tsc -p ./vendor/xbbcode/tsconfig.json; _exit_code=$?
if [[ $_exit_code -ne 0 ]]; then if [[ $_exit_code -ne 0 ]]; then
echo "Failed to build the XBBCode vendor" echo "Failed to build the XBBCode vendor"
exit 1 exit 1
fi fi
echo "Compile vendor emoji-picker"
execute_tsc ./vendor/emoji-picker/src/jquery.lsxemojipicker.ts
if [[ $_exit_code -ne 0 ]]; then
echo "Failed to build the emoji-picker vendor"
exit 1
fi
if [[ "$build_type" == "release" ]]; then # Compile everything for release mode if [[ "$build_type" == "release" ]]; then # Compile everything for release mode
echo "Packing generated css files" echo "Packing generated css files"
chmod +x ./shared/css/generate_packed.sh
./shared/css/generate_packed.sh; _exit_code=$? ./shared/css/generate_packed.sh; _exit_code=$?
if [[ $_exit_code -ne 0 ]]; then if [[ $_exit_code -ne 0 ]]; then
echo "Failed to package generated css files" echo "Failed to package generated css files"

View File

@ -1,13 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
function execute_tsc() { function execute_tsc() {
# shellcheck disable=SC2068
execute_npm_command tsc $@ execute_npm_command tsc $@
} }
function execute_ttsc() {
execute_npm_command ttsc $@
}
function execute_npm_command() { function execute_npm_command() {
command_name=$1 command_name=$1
command_variable="command_$command_name" command_variable="command_$command_name"

222
scripts/travis.sh Normal file
View File

@ -0,0 +1,222 @@
#!/bin/bash
LOG_FILE="auto-build/logs/build.log"
PACKAGES_DIRECTORY="auto-build/packages/"
build_verbose=1
build_release=1
build_debug=0
function print_help() {
echo "Possible arguments:"
echo " --verbose=[yes|no] | Enable verbose build output (Default: $build_verbose)"
echo " --enable-release=[yes|no] | Enable release build (Default: $build_release)"
echo " --enable-debug=[yes|no] | Enable debug build (Default: $build_debug)"
}
function parse_arguments() {
# Preprocess the help parameter
for argument in "$@"; do
if [[ "$argument" = "--help" ]] || [[ "$argument" = "-h" ]]; then
print_help
exit 1
fi
done
shopt -s nocasematch
for argument in "$@"; do
echo "Argument: $argument"
if [[ "$argument" =~ ^--verbose(=(y|1)?[[:alnum:]]*$)?$ ]]; then
build_verbose=0
if [[ -z "${BASH_REMATCH[1]}" ]] || [[ -n "${BASH_REMATCH[2]}" ]]; then
build_verbose=1
fi
if [[ ${build_verbose} ]]; then
echo "Enabled verbose output"
fi
elif [[ "$argument" =~ ^--enable-release(=(y|1)?[[:alnum:]]*$)?$ ]]; then
build_release=0
if [[ -z "${BASH_REMATCH[1]}" ]] || [[ -n "${BASH_REMATCH[2]}" ]]; then
build_release=1
fi
if [[ ${build_release} ]]; then
echo "Enabled release build!"
fi
elif [[ "$argument" =~ ^--enable-debug(=(y|1)?[[:alnum:]]*$)?$ ]]; then
build_debug=0
if [[ -z "${BASH_REMATCH[1]}" ]] || [[ -n "${BASH_REMATCH[2]}" ]]; then
build_debug=1
fi
if [[ ${build_debug} ]]; then
echo "Enabled debug build!"
fi
fi
done
}
function execute() {
time_begin=$(date +%s%N)
#Execute the command
if [[ "$#" -gt 2 ]]; then
echo "[EXECUTE] Executing commands:" >> ${LOG_FILE}
for command in "${@:2}"; do
echo "[EXECUTE] $command" >> ${LOG_FILE}
done
else
echo "[EXECUTE] Executing command \"$2\"" >> ${LOG_FILE}
fi
for command in "${@:2}"; do
echo "$> $command" >> ${LOG_FILE}
if [[ ${build_verbose} -gt 0 ]]; then
echo "$> $command"
fi
error=""
if [[ ${build_verbose} -gt 0 ]]; then
if [[ -f ${LOG_FILE}.tmp ]]; then
rm ${LOG_FILE}.tmp
fi
eval "${command}" |& tee ${LOG_FILE}.tmp | grep -E '^[^(/\S*/libstdc++.so\S*: no version information available)].*'
error_code=${PIPESTATUS[0]}
error=$(cat ${LOG_FILE}.tmp)
rm ${LOG_FILE}.tmp
else
error=$(eval "${command}" 2>&1)
error_code=$?
echo "$error" >> ${LOG_FILE}
fi
if [[ ${error_code} -ne 0 ]]; then
break
fi
done
#Log the result
time_end=$(date +%s%N)
time_needed=$((time_end - time_begin))
time_needed_ms=$((time_needed / 1000000))
echo "[EXECUTE] Command exited with exit code $error_code (Runtime ${time_needed_ms}ms)" >> ${LOG_FILE}
if [[ ${error_code} -ne 0 ]]; then
handle_failure ${error_code} "$1"
fi
echo "Command execution required ${time_needed_ms}ms"
error=""
}
function handle_failure() {
# We cut of the nasty "node: /usr/lib/libstdc++.so.6: no version information available (required by node)" message
echo "--------------------------- [ERROR] ---------------------------"
echo "We've encountered an fatal error, which isn't recoverable!"
echo " Aborting build process!"
echo ""
echo "Exit code : $1"
echo "Error message: ${*:2}"
if [[ ${build_verbose} -eq 0 ]] && [[ "$error" != "" ]]; then
echo "Command log: (lookup \"${LOG_FILE}\" for detailed output!)"
echo "$error" | grep -v 'libstdc++.so\S*: no version information available'
fi
echo "--------------------------- [ERROR] ---------------------------"
exit 1
}
cd "$(dirname "$0")/.." || { echo "Failed to enter base dir"; exit 1; }
error=""
LOG_FILE="$(pwd)/$LOG_FILE"
if [[ ! -d $(dirname "${LOG_FILE}") ]]; then
mkdir -p "$(dirname "${LOG_FILE}")"
fi
echo "Script arguments: $* ($#)"
if [[ "$1" == "bash" ]]; then
bash
exit 0
fi
parse_arguments "${@:1}"
if [[ -e "$LOG_FILE" ]]; then
rm "$LOG_FILE"
fi
echo "Updating project and submodules"
execute \
"Failed to update submodules" \
"git pull" \
"git submodule update --init --recursive --remote --checkout" \
"git status &>/dev/null" #We need this to "attach" to git else the git diff dosn't work
echo "---------- Native modules ---------- "
echo "Updating NPM"
execute \
"Failed to update npm" \
"npm install"
chmod +x ./web/native-codec/build.sh
execute \
"Failed to build native opus codec" \
"docker exec -it emscripten bash -c 'web/native-codec/build.sh'"
echo "---------- Web client ----------"
function move_target_file() {
file_name=$(ls -1t | grep -E "^TeaWeb-.*\.zip$" | head -n 1)
if [[ -z "$file_name" ]]; then
handle_failure -1 "Failed to find target file"
fi
mkdir -p "${PACKAGES_DIRECTORY}" || { echo "failed to create target path"; exit 1; }
target_file="${PACKAGES_DIRECTORY}/$file_name"
if [[ -f "$target_file" ]]; then
echo "Removing old packed file located at $target_file"
rm "${target_file}" && handle_failure -1 "Failed to remove target file"
fi
mv "${file_name}" "${target_file}"
echo "Moved target file to $target_file"
}
function execute_build_release() {
echo "Building release package"
execute \
"Failed to build release" \
"./scripts/build.sh web release"
echo "Packaging release"
execute \
"Failed to package release" \
"./scripts/web_package.sh release"
move_target_file
}
function execute_build_debug() {
echo "Building debug package"
execute \
"Failed to build debug" \
"./scripts/build.sh web dev"
echo "Packaging release"
execute \
"Failed to package debug" \
"./scripts/web_package.sh dev"
move_target_file
}
chmod +x ./scripts/build.sh
chmod +x ./scripts/web_package.sh
if [[ ${build_release} ]]; then
execute_build_release
fi
if [[ ${build_debug} ]]; then
execute_build_debug
fi
exit 0

View File

@ -1,5 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PACKAGES_DIRECTORY="auto-build/packages/"
LOG_FILE="auto-build/logs/build.log"
if [[ -z "${GIT_AUTHTOKEN}" ]]; then if [[ -z "${GIT_AUTHTOKEN}" ]]; then
echo "Missing environment variable GIT_AUTHTOKEN. Please set it before usign this script!" echo "Missing environment variable GIT_AUTHTOKEN. Please set it before usign this script!"
exit 1 exit 1
@ -23,11 +26,12 @@ if [[ ! -x ${GIT_RELEASE_EXECUTABLE} ]]; then
exit 1 exit 1
} }
gunzip /tmp/git-release.gz && chmod +x /tmp/git-release; gunzip /tmp/git-release.gz; _exit_code=$?;
[[ $? -eq 0 ]] || { [[ $_exit_code -eq 0 ]] || {
echo "Failed to unzip github-release-linux" echo "Failed to unzip github-release-linux"
exit 1 exit 1
} }
chmod +x /tmp/git-release;
echo "Download of github-release-linux (1.2.4) finished" echo "Download of github-release-linux (1.2.4) finished"
else else
@ -40,6 +44,7 @@ if [[ ! -x ${GIT_RELEASE_EXECUTABLE} ]]; then
fi fi
fi fi
cd "$(dirname "$0")/.." || { echo "Failed to enter base dir"; exit 1; }
echo "Generating release" echo "Generating release"
${GIT_RELEASE_EXECUTABLE} release \ ${GIT_RELEASE_EXECUTABLE} release \
--repo "TeaWeb" \ --repo "TeaWeb" \
@ -54,7 +59,7 @@ ${GIT_RELEASE_EXECUTABLE} release \
} }
echo "Uploading release files" echo "Uploading release files"
folders=("/tmp/build/logs/" "/tmp/build/packages/") folders=("${LOG_FILE}" "${PACKAGES_DIRECTORY}")
uploaded_files=() uploaded_files=()
failed_files=() failed_files=()

View File

@ -26,8 +26,7 @@ fi
response=$(git diff-index HEAD -- . ':!asm/libraries/' ':!package-lock.json' ':!vendor/') response=$(git diff-index HEAD -- . ':!asm/libraries/' ':!package-lock.json' ':!vendor/')
if [[ "$response" != "" ]]; then if [[ "$response" != "" ]]; then
echo "You're using a private modified build!" echo "You're using a private modified build! Cant assign git hash!"
echo "Cant assign git hash!"
NAME="TeaWeb-${type}.zip" NAME="TeaWeb-${type}.zip"
else else
NAME="TeaWeb-${type}-$(git rev-parse --short HEAD).zip" NAME="TeaWeb-${type}-$(git rev-parse --short HEAD).zip"
@ -41,7 +40,8 @@ fi
current_path=$(pwd) current_path=$(pwd)
cd "$source_path" || { echo "Failed to enter source path"; exit 1; } cd "$source_path" || { echo "Failed to enter source path"; exit 1; }
if zip -9 -r "${NAME}" ./*; then zip -9 -r "${NAME}" ./*; _exit_code=$?
if [[ $_exit_code -ne 0 ]]; then
echo "Failed to package environment!" echo "Failed to package environment!"
exit 1 exit 1
fi fi
@ -50,4 +50,4 @@ cd "$current_path" || { echo "Failed to reenter source path"; exit 1; }
mv "${source_path}/${NAME}" . mv "${source_path}/${NAME}" .
echo "Release package successfully packaged!" echo "Release package successfully packaged!"
echo "Target file: ${NAME}" echo "Target file: ${NAME} ($(pwd))"

5
shared/.gitignore vendored
View File

@ -20,4 +20,7 @@ popup/**/*.css.map
popup/**/*.js popup/**/*.js
popup/**/*.js.map popup/**/*.js.map
generated/* generated/*
/*.js
/*.js.map

View File

@ -190,7 +190,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
} }
} }
&:hover.displayed, &.force-show { &:hover.dropdownDisplayed, &.force-show {
.dropdown { .dropdown {
display: block; display: block;
} }

View File

@ -930,6 +930,9 @@
font-size: .85em; font-size: .85em;
color: #3c3c3c; color: #3c3c3c;
width: fit-content;
align-self: end;
} }
} }
} }

View File

@ -1,143 +0,0 @@
#!/usr/bin/env python2.7
"""
We want python 2.7 again...
"""
import io
import re
import json
import sys
"""
from googletrans import Translator # Use the free webhook
def run_translate(messages, source_language, target_language):
translator = Translator()
_translations = translator.translate(messages, src=source_language, dest=target_language)
result = []
for translation in _translations:
result.append({
"source": translation.origin,
"translated": translation.text
})
return result
"""
from google.cloud import translate # Use googles could solution
def run_translate(messages, source_language, target_language):
translate_client = translate.Client()
# The text to translate
text = u'Hello, world!'
# The target language
result = []
limit = 16
for chunk in [messages[i:i + limit] for i in xrange(0, len(messages), limit)]:
# Translates some text into Russian
print("Requesting {} translations".format(len(chunk)))
translations = translate_client.translate(chunk, target_language=target_language)
for translation in translations:
result.append({
"source": translation["input"],
"translated": translation["translatedText"]
})
return result
def translate_messages(source, destination, target_language):
with open(source) as f:
data = json.load(f)
result = {
"translations": [],
"info": None
}
try:
with open(destination) as f:
result = json.load(f)
print("loaded old result")
except:
pass
translations = result["translations"]
if translations is None:
print("Using new translation map")
translations = []
else:
print("Loaded {} old translations".format(len(translations)))
messages = []
for message in data:
try:
messages.index(message["message"])
except:
try:
found = False
for entry in translations:
if entry["key"]["message"] == message["message"]:
found = True
break
if not found:
raise Exception('add message for translate')
except:
messages.append(message["message"])
print("Translating {} messages".format(len(messages)))
if len(messages) != 0:
_translations = run_translate(messages, 'en', target_language)
print("Messages translated, generating target file")
for translation in _translations:
translations.append({
"key": {
"message": translation["source"]
},
"translated": translation["translated"],
"flags": [
"google-translate"
]
})
for translation in translations:
translation["translated"] = re.sub(r"% +([OoDdSs])", r" %\1", translation["translated"]) # Fix the broken "% o" or "% s" things
translation["translated"] = translation["translated"].replace("%O", "%o") # Replace all %O to %o
translation["translated"] = translation["translated"].replace("%S", "%s") # Replace all %S to %s
translation["translated"] = translation["translated"].replace("%D", "%d") # Replace all %D to %d
translation["translated"] = re.sub(r" +(%[ods])", r" \1", translation["translated"]) # Fix double spaces between a message and %s
translation["translated"] = re.sub(r"\( (%[ods])", r"(\1", translation["translated"]) # Fix the leading space after a brace: ( %s)
print("Writing target file")
result["translations"] = translations
if result["info"] is None:
result["info"] = {
"contributors": [
{
"name": "Google Translate, via script by Markus Hadenfeldt",
"email": "gtr.i18n.client@teaspeak.de"
}
],
"name": "Auto translated messages for language " + target_language
}
with io.open(destination, 'w', encoding='utf8') as f:
f.write(json.dumps(result, indent=2, ensure_ascii=False))
print("Done")
def main(target_language):
target_file = "i18n/{}_google_translate.translation".format(target_language)
translate_messages("generated/messages_script.json", target_file, target_language)
translate_messages("generated/messages_template.json", target_file, target_language)
pass
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Invalid argument count!")
print("Usage: ./generate_i18n_gtranslate.py <language>")
exit(1)
main(sys.argv[1])

View File

@ -0,0 +1,147 @@
import * as path from "path";
import * as fs from "fs-extra";
import {TranslationServiceClient} from "@google-cloud/translate";
const translation_project_id = "luminous-shadow-92008";
const translation_location = "global";
const translation_client = new TranslationServiceClient();
async function run_translate(messages: string[], source_language: string, target_language: string) : Promise<(string | undefined)[]> {
let messages_left = messages.slice(0);
let result = [];
while (messages_left.length > 0) {
const chunk_size = Math.min(messages_left.length, 128);
const chunk = messages_left.slice(0, chunk_size);
console.log("Translated %d/%d. Messages left: %d. Chunk size: %d", messages.length - messages_left.length, messages.length, messages_left.length, chunk_size);
try {
const [response] = await translation_client.translateText({
parent: `projects/${translation_project_id}/locations/${translation_location}`,
contents: chunk,
mimeType: "text/plain",
sourceLanguageCode: source_language,
targetLanguageCode: target_language
});
result.push(...response.translations.map(e => e.translatedText));
} catch (error) {
console.log(error);
console.log("Failed to execute translation request: %o", 'details' in error ? error.details : error instanceof Error ? error.message : error);
throw "translated failed";
}
messages_left = messages_left.slice(chunk_size);
}
return result;
}
interface TranslationFile {
info: {
name: string,
contributors: {
name: string,
email: string
}[]
},
translations: {
translated: string,
flags: string[],
key: {
message: string
}
}[]
}
interface InputFile {
message: string;
line: number;
character: number;
filename: string;
}
async function translate_messages(input_file: string, output_file: string, source_language: string, target_language: string) {
let output_data: TranslationFile;
if(await fs.pathExists(output_file)) {
try {
output_data = await fs.readJSON(output_file);
} catch (error) {
console.log("Failed to parse output data: %o", error);
throw "failed to read output file";
}
} else {
output_data = {} as any;
}
if(!output_data.info) {
output_data.info = {
contributors: [
{
"name": "Google Translate, via script by Markus Hadenfeldt",
"email": "gtr.i18n.client@teaspeak.de"
}
],
name: "Auto translated messages for language " + target_language
}
}
if(!Array.isArray(output_data.translations))
output_data.translations = [];
let messages_to_translate: InputFile[] = [];
try {
messages_to_translate = await fs.readJSON(input_file);
} catch (error) {
console.log("Failed to parse input file %o", error);
throw "failed to read input file";
}
const original_messages = messages_to_translate.length;
messages_to_translate = messages_to_translate.filter(e => output_data.translations.findIndex(f => e.message === f.key.message) === -1);
console.log("Messages to translate: %d out of %d", messages_to_translate.length, original_messages);
const response = await run_translate(messages_to_translate.map(e => e.message), source_language, target_language);
if(messages_to_translate.length !== response.length)
throw "invalid response length";
for(let index = 0; index < response.length; index++) {
if(typeof response[index] !== "string") {
console.log("Failed to translate message %s", messages_to_translate[index]);
continue;
}
output_data.translations.push({
key: {
message: messages_to_translate[index].message
},
translated: response[index],
flags: [
"google-translated"
]
});
}
await fs.writeJSON(output_file, output_data, {
spaces: " "
});
}
const process_args = process.argv.slice(2);
if(process_args.length < 1) {
console.error("Invalid argument count");
console.error("Usage: ./generate_i18n_gtranslate.py <language> [<target file>]");
process.exit(1);
}
const input_files = ["../dist/translations.json", "generated/translations_html.json"].map(e => path.join(__dirname, e));
const output_file = process_args[1] || path.join(__dirname, "i18n", process_args[0] + "_google_translate.translation");
(async () => {
for(const file of input_files)
await translate_messages(file, output_file, "en", process_args[0]);
})().catch(error => {
console.error("Failed to create translation files: %o", error);
process.exit(1);
}).then(() => {
console.log("Translation files have been updated.");
process.exit(0);
});

View File

@ -1,17 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
BASEDIR=$(dirname "$0") cd "$(dirname "$0")" || { echo "Failed to enter base directory"; exit 1; }
cd "$BASEDIR"
#Generate the script translations npm run trgen -- -f "$(pwd)/html/templates.html" -f "$(pwd)/html/templates/modal/newcomer.html" -f "$(pwd)/html/templates/modal/musicmanage.html" -d "$(pwd)/generated/translations_html.json"; _exit_code=$?
npm run ttsc -- -p $(pwd)/tsconfig/tsconfig.json if [[ $_exit_code -ne 0 ]]; then
if [ $? -ne 0 ]; then
echo "Failed to generate translation file for the script files"
exit 1
fi
npm run trgen -- -f $(pwd)/html/templates.html -d $(pwd)/generated/messages_template.json
if [ $? -ne 0 ]; then
echo "Failed to generate translations file for the template files" echo "Failed to generate translations file for the template files"
exit 1 exit 1
fi fi

View File

@ -12,119 +12,6 @@
<!-- navigation bar --> <!-- navigation bar -->
<div class="container-control-bar"> <div class="container-control-bar">
<div id="control_bar" class="control_bar"> <div id="control_bar" class="control_bar">
{{if multi_session}}
<div class="button-dropdown container-connect" title="{{tr 'Connect to a server' /}}">
<div class="buttons">
<div class="button btn_connect">
<div class="icon_em client-connect"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_connect">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server" /}}</a></div>
<div class="btn_connect_new_tab">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server in another tab" /}}</a></div>
</div>
</div>
<div class="button-dropdown container-disconnect" title="{{tr 'Disconnect from server' /}}"
style="display: none">
<div class="buttons">
<div class="button btn_disconnect">
<div class="icon_em client-disconnect"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_disconnect">
<div class="icon client-disconnect"></div>
<a>{{tr "Disconnect from current server" /}}</a></div>
<div class="btn_connect">
<div class="icon client-connect"></div>
<a>{{tr "Connect to a server" /}}</a></div>
</div>
</div>
{{else}}
<div class="button container-connect btn_connect">
<div class="icon_em client-connect" title="{{tr 'Connect to a server' /}}"></div>
</div>
<div class="button container-disconnect btn_disconnect">
<div class="icon_em client-disconnect" title="{{tr 'Disconnect from server' /}}"></div>
</div>
{{/if}}
<div class="button-dropdown btn_bookmark" title="{{tr 'Bookmarks' /}}">
<div class="buttons">
<div class="button btn_bookmark_list">
<div class="icon_em client-bookmark_manager"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<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>
<hr>
</div>
</div>
<div class="divider"></div>
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="buttons">
<div class="button btn_away_toggle">
<div class="icon_em client-away"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown" style="width: 350px">
<div class="btn_away_disable">
<div class="icon client-present"></div>
<a>{{tr "Go online" /}}</a></div>
<div class="btn_away_enable">
<div class="icon client-away"></div>
<a>{{tr "Set away on this server" /}}</a></div>
<div class="btn_away_message">
<div class="icon client-away"></div>
<a>{{tr "Set away message on this server" /}}</a></div>
<hr class="btn_away_message_global">
<!-- applied to this HR this class because it needs to get hidden as well if we dont have global settings -->
<div class="btn_away_enable_global">
<div class="icon client-away"></div>
<a>{{tr "Set away for all servers" /}}</a></div>
<div class="btn_away_message_global">
<div class="icon client-away"></div>
<a>{{tr "Set away message for all servers" /}}</a></div>
<div class="btn_away_disable_global">
<div class="icon client-present"></div>
<a>{{tr "Go online for all servers" /}}</a></div>
</div>
</div>
<div class="button button-red btn_mute_input">
<div class="icon_em client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
</div>
<div class="button button-red btn_mute_output">
<div class="icon_em client-output_muted"
title="{{tr 'Mute/unmute headphones' /}}"></div>
</div>
<!-- <!--
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}"> <div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
<div class="buttons"> <div class="buttons">
@ -148,38 +35,6 @@
</div> </div>
<div class="divider"></div> <div class="divider"></div>
--> -->
<div class="divider"></div>
<div class="button button-subscribe-mode">
<div class="icon_em" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
</div>
<!-- the query button -->
<div class="button-dropdown btn_query" title="{{tr 'Show/hide server queries' /}}">
<div class="buttons">
<div class="button btn_query_toggle">
<div class="icon_em client-server_query"></div>
</div>
<div class="dropdown-arrow">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
<div class="btn_query_toggle">
<div class="icon client-toggle_server_query_clients"></div>
<a class="query-text"></a></div>
<div class="btn_query_manage">
<div class="icon client-server_query"></div>
<a>{{tr "Manage server queries" /}}</a></div>
<!-- <div class="btn_query_create"><div class="icon client-away"></div><a>{{tr "Create server query login" /}}</a></div> -->
</div>
</div>
<div style="width: 100%"></div>
<!-- -->
<div class="button button-hostbutton" title="{{tr 'Hostbutton' /}}">
<img alt="{{tr 'hostbutton' /}}">
</div>
</div> </div>
</div> </div>
<div class="container-connection-handlers scrollbar" id="connection-handlers"> <div class="container-connection-handlers scrollbar" id="connection-handlers">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,30 @@
"email": "gtr.i18n.client@teaspeak.de" "email": "gtr.i18n.client@teaspeak.de"
} }
] ]
}, {
"key": "hu_gt",
"country_code": "hu",
"path": "hu_google_translate.translation",
"name": "Auto translated messages for language hu (Hungarian)",
"contributors": [
{
"name": "Google Translate, via script by Markus Hadenfeldt",
"email": "gtr.i18n.client@teaspeak.de"
}
]
}, {
"key": "pt_gt",
"country_code": "pt",
"path": "pt_google_translate.translation",
"name": "Auto translated messages for language pt (Portuguese)",
"contributors": [
{
"name": "Google Translate, via script by Markus Hadenfeldt",
"email": "gtr.i18n.client@teaspeak.de"
}
]
} }
], ],
"name": "Default TeaSpeak repository", "name": "Default TeaSpeak repository",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,11 +26,12 @@ import {Frame} from "tc-shared/ui/frames/chat_frame";
import {Hostbanner} from "tc-shared/ui/frames/hostbanner"; import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect"; import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import {formatMessage} from "tc-shared/ui/frames/chat"; import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar"; import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
import * as connection from "tc-backend/connection"; import * as connection from "tc-backend/connection";
import * as dns from "tc-backend/dns"; import * as dns from "tc-backend/dns";
import * as top_menu from "tc-shared/ui/frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export enum DisconnectReason { export enum DisconnectReason {
HANDLER_DESTROYED, HANDLER_DESTROYED,
@ -344,10 +345,7 @@ export class ConnectionHandler {
} }
} }
if(update_control && server_connections.active_connection_handler() === this) { control_bar_instance()?.events().fire("server_updated", { category: "settings-initialized", handler: this });
control_bar.apply_server_state();
}
} }
get connected() : boolean { get connected() : boolean {
@ -382,7 +380,8 @@ export class ConnectionHandler {
if(pathname.endsWith(".php")) if(pathname.endsWith(".php"))
pathname = pathname.substring(0, pathname.lastIndexOf("/")); pathname = pathname.substring(0, pathname.lastIndexOf("/"));
if(bipc.supported()) { /* certaccept is currently not working! */
if(bipc.supported() && false) {
tag.attr('href', "#"); tag.attr('href', "#");
let popup: Window; let popup: Window;
tag.on('click', event => { tag.on('click', event => {
@ -607,8 +606,7 @@ export class ConnectionHandler {
if(this.serverConnection) if(this.serverConnection)
this.serverConnection.disconnect(); this.serverConnection.disconnect();
if(control_bar.current_connection_handler() == this) this.on_connection_state_changed(); /* really required to call? */
control_bar.update_connection_state();
this.side_bar.private_conversations().clear_client_ids(); this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update(); this.hostbanner.update();
@ -642,8 +640,7 @@ export class ConnectionHandler {
} }
private on_connection_state_changed() { private on_connection_state_changed() {
if(control_bar.current_connection_handler() == this) control_bar_instance()?.events().fire("server_updated", { category: "connection-state", handler: this });
control_bar.update_connection_state();
} }
private _last_record_error_popup: number; private _last_record_error_popup: number;
@ -751,8 +748,9 @@ export class ConnectionHandler {
} }
} }
if(control_bar.current_connection_handler() === this)
control_bar.apply_server_voice_state(); control_bar_instance()?.events().fire("server_updated", { category: "audio", handler: this });
top_menu.update_state(); //TODO: Only run "small" update?
} }
sync_status_with_server() { sync_status_with_server() {
@ -770,7 +768,7 @@ export class ConnectionHandler {
}); });
} }
set_away_status(state: boolean | string) { set_away_status(state: boolean | string, update_control_bar: boolean) {
if(this.client_status.away === state) if(this.client_status.away === state)
return; return;
@ -789,7 +787,8 @@ export class ConnectionHandler {
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
}); });
control_bar.update_button_away(); if(update_control_bar)
control_bar_instance()?.events().fire("server_updated", { category: "away-status", handler: this });
} }
resize_elements() { resize_elements() {

View File

@ -216,6 +216,7 @@ export namespace bbcode {
} }
}); });
const load_callback = guid();
/* the image parse & displayer */ /* the image parse & displayer */
xbbcode.register.register_parser({ xbbcode.register.register_parser({
tag: ["img", "image"], tag: ["img", "image"],
@ -238,10 +239,11 @@ export namespace bbcode {
return fallback_value; return fallback_value;
} }
sanitizer_escaped_map[uid] = "<div class='xbbcode-tag-img'><img src='img/loading_image.svg' onload='messages.formatter.bbcode.load_image(this)' x-image-url='" + encodeURIComponent(target) + "' title='" + sanitize_text(target) + "' /></div>"; sanitizer_escaped_map[uid] = "<div class='xbbcode-tag-img'><img src='img/loading_image.svg' onload='window[\"" + load_callback + "\"](this)' x-image-url='" + encodeURIComponent(target) + "' title='" + sanitize_text(target) + "' /></div>";
return sanitizer_escaped(uid); return sanitizer_escaped(uid);
} }
}) });
window[load_callback] = load_image;
}, },
priority: 10 priority: 10
}); });

View File

@ -52,7 +52,7 @@ export enum BookmarkType {
} }
export interface Bookmark { export interface Bookmark {
type: /* BookmarkType.ENTRY */ BookmarkType; type: BookmarkType.ENTRY;
/* readonly */ parent: DirectoryBookmark; /* readonly */ parent: DirectoryBookmark;
server_properties: ServerProperties; server_properties: ServerProperties;
@ -70,7 +70,7 @@ export interface Bookmark {
} }
export interface DirectoryBookmark { export interface DirectoryBookmark {
type: /* BookmarkType.DIRECTORY */ BookmarkType; type: BookmarkType.DIRECTORY;
/* readonly */ parent: DirectoryBookmark; /* readonly */ parent: DirectoryBookmark;
readonly content: (Bookmark | DirectoryBookmark)[]; readonly content: (Bookmark | DirectoryBookmark)[];

View File

@ -58,6 +58,8 @@ export abstract class AbstractCommandHandlerBoss {
register_single_handler(handler: SingleCommandHandler) { register_single_handler(handler: SingleCommandHandler) {
if(typeof handler.command === "string")
handler.command = [handler.command];
this.single_command_handler.push(handler); this.single_command_handler.push(handler);
} }
@ -82,7 +84,8 @@ export abstract class AbstractCommandHandlerBoss {
} }
for(const handler of [...this.single_command_handler]) { for(const handler of [...this.single_command_handler]) {
if(handler.command && handler.command != command.command) // We already know that handler.command must be an array (It will be converted within the register single handler methode)
if(handler.command && (handler.command as string[]).findIndex(e => e === command.command) == -1)
continue; continue;
try { try {

View File

@ -263,38 +263,53 @@ export class CommandHelper extends AbstractCommandHandler {
} }
request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> { request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> {
let bulked_response = false;
let bulk_index = 0;
const result: PlaylistSong[] = [];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const single_handler: SingleCommandHandler = { const single_handler: SingleCommandHandler = {
command: "notifyplaylistsonglist", command: ["notifyplaylistsonglist", "notifyplaylistsonglistfinished"],
function: command => { function: command => {
const json = command.arguments; const json = command.arguments;
if(json[0]["playlist_id"] != playlist_id) { if(bulk_index === 0) {
log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs")); /* we're sending the response as bulk */
return false; bulked_response = parseInt(json[0]["version"]) >= 2;
} }
const result: PlaylistSong[] = []; if(parseInt(json[0]["playlist_id"]) !== playlist_id)
return false; /* not our request */
for(const entry of json) { if(command.command === "notifyplaylistsonglistfinished") {
try { resolve(result);
result.push({ return true;
song_id: parseInt(entry["song_id"]), } else {
song_invoker: entry["song_invoker"], for(const entry of json) {
song_previous_song_id: parseInt(entry["song_previous_song_id"]), try {
song_url: entry["song_url"], result.push({
song_url_loader: entry["song_url_loader"], song_id: parseInt(entry["song_id"]),
song_invoker: entry["song_invoker"],
song_previous_song_id: parseInt(entry["song_previous_song_id"]),
song_url: entry["song_url"],
song_url_loader: entry["song_url_loader"],
song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1", song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1",
song_metadata: entry["song_metadata"] song_metadata: entry["song_metadata"]
}); });
} catch(error) { } catch(error) {
log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error); log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error);
}
}
if(bulked_response) {
bulk_index++;
return false;
} else {
resolve(result);
return true;
} }
} }
resolve(result);
return true;
} }
}; };
this.handler_boss.register_single_handler(single_handler); this.handler_boss.register_single_handler(single_handler);

View File

@ -121,7 +121,7 @@ export class ServerCommand {
export interface SingleCommandHandler { export interface SingleCommandHandler {
name?: string; name?: string;
command?: string; command?: string | string[];
timeout?: number; timeout?: number;
/* if the return is true then the command handler will be removed */ /* if the return is true then the command handler will be removed */

View File

@ -1,29 +1,37 @@
//TODO: Combine EventConvert and Event?
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client"; import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
import {guid} from "tc-shared/crypto/uid"; import {guid} from "tc-shared/crypto/uid";
import * as React from "react";
export interface EventConvert<All> { export interface Event<Events, T = keyof Events> {
as<T extends keyof All>() : All[T];
}
export interface Event<T> {
readonly type: T; readonly type: T;
as<T extends keyof Events>() : Events[T];
} }
export class SingletonEvent implements Event<"singletone-instance"> { interface SingletonEvents {
"singletone-instance": never;
}
export class SingletonEvent implements Event<SingletonEvents, "singletone-instance"> {
static readonly instance = new SingletonEvent(); static readonly instance = new SingletonEvent();
readonly type = "singletone-instance"; readonly type = "singletone-instance";
private constructor() { } private constructor() { }
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
} }
const event_annotation_key = guid();
export class Registry<Events> { export class Registry<Events> {
private readonly registry_uuid; private readonly registry_uuid;
private handler: {[key: string]: ((event) => void)[]} = {}; private handler: {[key: string]: ((event) => void)[]} = {};
private connections: {[key: string]:Registry<string>[]} = {}; private connections: {[key: string]:Registry<string>[]} = {};
private event_handler_objects: {
object: any,
handlers: {[key: string]: ((event) => void)[]}
}[] = [];
private debug_prefix = undefined; private debug_prefix = undefined;
private warn_unhandled_events = true;
constructor() { constructor() {
this.registry_uuid = "evreg_data_" + guid(); this.registry_uuid = "evreg_data_" + guid();
@ -33,8 +41,11 @@ export class Registry<Events> {
enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; } enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; }
disable_debug() { this.debug_prefix = undefined; } disable_debug() { this.debug_prefix = undefined; }
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<T> & EventConvert<Events>) => void); enable_warn_unhandled_events() { this.warn_unhandled_events = true; }
on(events: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void); disable_warn_unhandled_events() { this.warn_unhandled_events = false; }
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
on(events, handler) { on(events, handler) {
if(!Array.isArray(events)) if(!Array.isArray(events))
events = [events]; events = [events];
@ -49,8 +60,8 @@ export class Registry<Events> {
} }
/* one */ /* one */
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<T> & EventConvert<Events>) => void); one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
one(events: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void); one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
one(events, handler) { one(events, handler) {
if(!Array.isArray(events)) if(!Array.isArray(events))
events = [events]; events = [events];
@ -63,9 +74,9 @@ export class Registry<Events> {
} }
} }
off<T extends keyof Events>(handler: (event?: Event<T>) => void); off<T extends keyof Events>(handler: (event?: Event<Events, T>) => void);
off<T extends keyof Events>(event: T, handler: (event?: Event<T> & EventConvert<Events>) => void); off<T extends keyof Events>(event: T, handler: (event?: Event<Events, T>) => void);
off(event: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void); off(event: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
off(handler_or_events, handler?) { off(handler_or_events, handler?) {
if(typeof handler_or_events === "function") { if(typeof handler_or_events === "function") {
for(const key of Object.keys(this.handler)) for(const key of Object.keys(this.handler))
@ -81,12 +92,14 @@ export class Registry<Events> {
} }
} }
connect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) { connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any); for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
} }
disconnect<EOther, T extends keyof Events & keyof EOther>(event: T, target: Registry<EOther>) { disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
(this.connections[event as string] || []).remove(target as any); for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || []).remove(target as any);
} }
disconnect_all<EOther>(target: Registry<EOther>) { disconnect_all<EOther>(target: Registry<EOther>) {
@ -102,24 +115,113 @@ export class Registry<Events> {
as: function () { return this; } as: function () { return this; }
}); });
let invoke_count = 0;
for(const handler of (this.handler[event_type as string] || [])) { for(const handler of (this.handler[event_type as string] || [])) {
handler(event); handler(event);
invoke_count++;
const reg_data = handler[this.registry_uuid]; const reg_data = handler[this.registry_uuid];
if(typeof reg_data === "object" && reg_data.singleshot) if(typeof reg_data === "object" && reg_data.singleshot)
this.handler[event_type as string].remove(handler); this.handler[event_type as string].remove(handler);
} }
for(const evhandler of (this.connections[event_type as string] || [])) for(const evhandler of (this.connections[event_type as string] || [])) {
evhandler.fire(event_type as any, event as any); evhandler.fire(event_type as any, event as any);
invoke_count++;
}
if(invoke_count === 0) {
console.warn("Event handler (%s) triggered event %s which has no consumers.", this.debug_prefix, event_type);
}
} }
fire_async<T extends keyof Events>(event_type: T, data?: Events[T]) { fire_async<T extends keyof Events>(event_type: T, data?: Events[T]) {
setTimeout(() => this.fire(event_type, data)); setTimeout(() => this.fire(event_type, data));
} }
destory() { destroy() {
this.handler = {}; this.handler = {};
this.connections = {};
this.event_handler_objects = [];
}
register_handler(handler: any) {
if(typeof handler !== "object")
throw "event handler must be an object";
const proto = Object.getPrototypeOf(handler);
if(typeof proto !== "object")
throw "event handler must have a prototype";
let registered_events = {};
for(const function_name of Object.getOwnPropertyNames(proto)) {
if(function_name === "constructor") continue;
if(typeof proto[function_name][event_annotation_key] !== "object") continue;
const event_data = proto[function_name][event_annotation_key];
const ev_handler = event => proto[function_name].call(handler, event);
for(const event of event_data.events) {
registered_events[event] = registered_events[event] || [];
registered_events[event].push(ev_handler);
this.on(event, ev_handler);
}
}
if(Object.keys(registered_events).length === 0)
throw "no events found in event handler";
this.event_handler_objects.push({
handlers: registered_events,
object: handler
});
}
unregister_handler(handler: any) {
const data = this.event_handler_objects.find(e => e.object === handler);
if(!data) throw "unknown event handler";
this.event_handler_objects.remove(data);
for(const key of Object.keys(data.handlers)) {
for(const evhandler of data.handlers[key])
this.off(evhandler);
}
}
}
export function EventHandler<EventTypes>(events: (keyof EventTypes) | (keyof EventTypes)[]) {
return function (target: any,
propertyKey: string,
descriptor: PropertyDescriptor) {
if(typeof target[propertyKey] !== "function")
throw "Invalid event handler annotation. Expected to be on a function type.";
target[propertyKey][event_annotation_key] = {
events: Array.isArray(events) ? events : [events]
};
}
}
export function ReactEventHandler<ObjectClass = React.Component<any, any>, EventTypes = any>(registry_callback: (object: ObjectClass) => Registry<EventTypes>) {
return function (constructor: Function) {
if(!React.Component.prototype.isPrototypeOf(constructor.prototype))
throw "Class/object isn't an instance of React.Component";
const didMount = constructor.prototype.componentDidMount;
constructor.prototype.componentDidMount = function() {
const registry = registry_callback(this);
if(!registry) throw "Event registry returned for an event object is invalid";
registry.register_handler(this);
if(typeof didMount === "function")
didMount.call(this, arguments);
};
const willUnmount = constructor.prototype.componentWillUnmount;
constructor.prototype.componentWillUnmount = function () {
const registry = registry_callback(this);
if(!registry) throw "Event registry returned for an event object is invalid";
registry.unregister_handler(this);
if(typeof willUnmount === "function")
willUnmount.call(this, arguments);
};
} }
} }
@ -595,11 +697,10 @@ export namespace modal {
} }
} }
/*
//Some test code //Some test code
const eclient = new events.Registry<events.channel_tree.client>(); const eclient = new Registry<channel_tree.client>();
const emusic = new events.Registry<events.sidebar.music>(); const emusic = new Registry<sidebar.music>();
eclient.on("property_update", event => { event.as<"playlist_song_loaded">(); });
eclient.connect("playlist_song_loaded", emusic); eclient.connect("playlist_song_loaded", emusic);
eclient.connect("playlist_song_loaded", emusic); eclient.connect("playlist_song_loaded", emusic);
*/

View File

@ -0,0 +1,237 @@
import {Registry} from "tc-shared/events";
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents";
import {control_bar_instance, ControlBarEvents} from "tc-shared/ui/frames/control-bar";
import {manager, Sound} from "tc-shared/sound/Sounds";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {Settings, settings} from "tc-shared/settings";
import {add_current_server} from "tc-shared/bookmarks";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import PermissionType from "tc-shared/permission/PermissionType";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {openBanList} from "tc-shared/ui/modal/ModalBanList";
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
{
let microphone_muted = undefined;
event_registry.on("action_toggle_speaker", event => {
if(microphone_muted === event.state) return;
if(typeof microphone_muted !== "undefined")
manager.play(event.state ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
microphone_muted = event.state;
})
}
{
let speakers_muted = undefined;
event_registry.on("action_toggle_microphone", event => {
if(speakers_muted === event.state) return;
if(typeof speakers_muted !== "undefined")
manager.play(event.state ? Sound.SOUND_MUTED : Sound.SOUND_ACTIVATED);
speakers_muted = event.state;
})
}
}
function load_default_states() {
this.event_registry.fire("action_toggle_speaker", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) });
this.event_registry.fire("action_toggle_microphone", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) });
}
export function initialize(event_registry: Registry<ClientGlobalControlEvents>) {
let current_connection_handler: ConnectionHandler | undefined;
event_registry.on("action_set_active_connection_handler", event => { current_connection_handler = event.handler; });
initialize_sounds(event_registry);
/* away state handler */
event_registry.on("action_set_away", event => {
const set_away = message => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(typeof message === "string" && !!message ? message : true, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
};
if(event.prompt_reason) {
createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => {
if(typeof(message) === "string")
set_away(message);
}).open();
} else {
set_away(undefined);
}
});
event_registry.on("action_disable_away", event => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(false, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
});
event_registry.on("action_toggle_microphone", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, !event.state);
if(current_connection_handler) {
current_connection_handler.client_status.input_muted = !event.state;
if(!current_connection_handler.client_status.input_hardware)
current_connection_handler.acquire_recorder(default_recorder, true); /* acquire_recorder already updates the voice status */
else
current_connection_handler.update_voice_status(undefined);
}
});
event_registry.on("action_toggle_speaker", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, !event.state);
if(!current_connection_handler) return;
current_connection_handler.client_status.output_muted = !event.state;
current_connection_handler.update_voice_status(undefined);
});
event_registry.on("action_set_channel_subscribe_mode", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.channel_subscribe_all = event.subscribe;
if(event.subscribe)
current_connection_handler.channelTree.subscribe_all_channels();
else
current_connection_handler.channelTree.unsubscribe_all_channels(true);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, event.subscribe);
});
event_registry.on("action_toggle_query", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.queries_visible = event.shown;
current_connection_handler.channelTree.toggle_server_queries(event.shown);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_SHOW_QUERIES, event.shown);
});
event_registry.on("action_add_current_server_to_bookmarks", () => add_current_server());
event_registry.on("action_open_connect", event => {
current_connection_handler?.cancel_reconnect(true);
spawnConnectModal({
default_connect_new_tab: event.new_tab
}, {
url: "ts.TeaSpeak.de",
enforce: false
});
});
event_registry.on("action_open_window", event => {
const handle_import_error = error => {
console.error("Failed to import script: %o", error);
createErrorModal(tr("Failed to load window"), tr("Failed to load the bookmark window.\nSee the console for more details.")).open();
};
const connection_handler = event.connection || current_connection_handler;
switch (event.window) {
case "bookmark-manage":
import("../ui/modal/ModalBookmarks").catch(error => {
handle_import_error(error);
return undefined;
}).then(window => {
window?.spawnBookmarkModal();
});
break;
case "query-manage":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
import("../ui/modal/ModalQueryManage").catch(error => {
handle_import_error(error);
return undefined;
}).then(window => {
window?.spawnQueryManage(connection_handler);
});
break;
case "query-create":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
spawnQueryCreate(connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
break;
case "ban-list":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
openBanList(connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
break;
case "permissions":
if(!connection_handler || !connection_handler.connected) {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
return;
}
if(connection_handler)
spawnPermissionEdit(connection_handler).open();
else
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
break;
case "token-list":
createErrorModal(tr("Not implemented"), tr("Token list is not implemented yet!")).open();
break;
case "token-use":
//FIXME: Move this out to a dedicated method
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
if(connection_handler.serverConnection.connected)
connection_handler.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
break;
case "settings":
spawnSettingsModal();
break;
default:
console.warn(tr("Received open window event for an unknown window: %s"), event.window);
}
});
load_default_states();
}

View File

@ -0,0 +1,49 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export interface ClientGlobalControlEvents {
action_set_channel_subscribe_mode: {
subscribe: boolean
},
action_disconnect: {
globally: boolean
},
action_open_connect: {
new_tab: boolean
},
action_toggle_microphone: {
state: boolean
},
action_toggle_speaker: {
state: boolean
},
action_disable_away: {
globally: boolean
},
action_set_away: {
globally: boolean;
prompt_reason: boolean;
},
action_toggle_query: {
shown: boolean
},
action_open_window: {
window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings",
connection?: ConnectionHandler
},
action_add_current_server_to_bookmarks: {},
action_set_active_connection_handler: {
handler?: ConnectionHandler
},
//TODO
notify_microphone_state_changed: {
state: boolean
}
}

View File

@ -16,7 +16,6 @@ import * as fidentity from "./profiles/identities/TeaForumIdentity";
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile"; import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
import * as cmanager from "tc-shared/ui/frames/connection_handlers"; import * as cmanager from "tc-shared/ui/frames/connection_handlers";
import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers"; import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers";
import * as control_bar from "tc-shared/ui/frames/ControlBar";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar"; import * as top_menu from "./ui/frames/MenuBar";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
@ -26,9 +25,16 @@ import * as aplayer from "tc-backend/audio/player";
import * as arecorder from "tc-backend/audio/recorder"; import * as arecorder from "tc-backend/audio/recorder";
import * as ppt from "tc-backend/ppt"; import * as ppt from "tc-backend/ppt";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as cbar from "./ui/frames/control-bar";
import {Registry} from "tc-shared/events";
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents";
/* required import for init */ /* required import for init */
require("./proto").initialize(); require("./proto").initialize();
require("./ui/elements/ContextDivider").initialize(); require("./ui/elements/ContextDivider").initialize();
require("./ui/elements/Tab");
require("./connection/CommandHandler"); /* else it might not get bundled because only the backends are accessing it */ require("./connection/CommandHandler"); /* else it might not get bundled because only the backends are accessing it */
const js_render = window.jsrender || $; const js_render = window.jsrender || $;
@ -140,6 +146,8 @@ async function initialize() {
bipc.setup(); bipc.setup();
} }
export let client_control_events: Registry<ClientGlobalControlEvents>;
async function initialize_app() { async function initialize_app() {
try { //Initialize main template try { //Initialize main template
const main = $("#tmpl_main").renderTag({ const main = $("#tmpl_main").renderTag({
@ -154,7 +162,15 @@ async function initialize_app() {
return; return;
} }
control_bar.set_control_bar(new control_bar.ControlBar($("#control_bar"))); /* setup the control bar */ client_control_events = new Registry<ClientGlobalControlEvents>();
{
const bar = (
<cbar.ControlBar ref={cbar.react_reference()} multiSession={true} />
);
ReactDOM.render(bar, $(".container-control-bar")[0]);
cbar.control_bar_instance().load_default_states();
}
if(!aplayer.initialize()) if(!aplayer.initialize())
console.warn(tr("Failed to initialize audio controller!")); console.warn(tr("Failed to initialize audio controller!"));
@ -333,7 +349,7 @@ function main() {
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]); server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]);
(<any>window).test_upload = (message?: string) => { (window as any).test_upload = (message?: string) => {
message = message || "Hello World"; message = message || "Hello World";
const connection = server_connections.active_connection_handler(); const connection = server_connections.active_connection_handler();

View File

@ -167,7 +167,7 @@ export class GroupManager extends AbstractCommandHandler {
} }
let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]); let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]);
for(let key in Object.keys(groupData)) { for(let key of Object.keys(groupData)) {
if(key == "sgid") continue; if(key == "sgid") continue;
if(key == "cgid") continue; if(key == "cgid") continue;
if(key == "type") continue; if(key == "type") continue;

View File

@ -1,6 +1,6 @@
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory, LogType} from "tc-shared/log"; import {LogCategory, LogType} from "tc-shared/log";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {LaterPromise} from "tc-shared/utils/LaterPromise"; import {LaterPromise} from "tc-shared/utils/LaterPromise";
import {ServerCommand} from "tc-shared/connection/ConnectionBase"; import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";

View File

@ -2,7 +2,7 @@ import {ChannelTree} from "tc-shared/ui/view";
import {ClientEntry} from "tc-shared/ui/client"; import {ClientEntry} from "tc-shared/ui/client";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory, LogType} from "tc-shared/log"; import {LogCategory, LogType} from "tc-shared/log";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {settings, Settings} from "tc-shared/settings"; import {settings, Settings} from "tc-shared/settings";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {Sound} from "tc-shared/sound/Sounds"; import {Sound} from "tc-shared/sound/Sounds";

View File

@ -0,0 +1,28 @@
import * as React from "react";
export abstract class ReactComponentBase<Properties, State> extends React.Component<Properties, State> {
constructor(props: Properties) {
super(props);
this.state = this.default_state();
this.initialize();
}
protected initialize() { }
protected abstract default_state() : State;
updateState(updates: {[key in keyof State]?: State[key]}) {
this.setState(Object.assign(this.state, updates));
}
protected classList(...classes: (string | undefined)[]) {
return [...classes].filter(e => typeof e === "string" && e.length > 0).join(" ");
}
protected hasChildren() {
const type = typeof this.props.children;
if(type === "undefined") return false;
return Array.isArray(this.props.children) ? this.props.children.length > 0 : true;
}
}

View File

@ -73,7 +73,7 @@ export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
const parent_offset = slider.offset(); const parent_offset = slider.offset();
const min = parent_offset.left; const min = parent_offset.left;
const max = parent_offset.left + slider.width(); const max = parent_offset.left + slider.width();
const current = event instanceof MouseEvent ? event.pageX : event.touches[event.touches.length - 1].clientX; const current = 'touches' in event ? event.touches[event.touches.length - 1].clientX : event.pageX;
const range = options.max_value - options.min_value; const range = options.max_value - options.min_value;
const offset = Math.round(((current - min) * (range / options.step)) / (max - min)) * options.step; const offset = Math.round(((current - min) * (range / options.step)) / (max - min)) * options.step;
@ -83,7 +83,7 @@ export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
update_value(value, true); update_value(value, true);
}; };
slider.on('mousedown', event => { slider.on('mousedown touchstart', ((event: MouseEvent | TouchEvent) => {
document.addEventListener('mousemove', mouse_listener); document.addEventListener('mousemove', mouse_listener);
document.addEventListener('touchmove', mouse_listener); document.addEventListener('touchmove', mouse_listener);
@ -93,7 +93,9 @@ export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
tool.show(); tool.show();
slider.addClass("active"); slider.addClass("active");
});
mouse_listener(event);
}) as any);
update_value(options.initial_value, false); update_value(options.initial_value, false);

View File

@ -0,0 +1,14 @@
import * as React from "react";
export class Translatable extends React.Component<{ message: string }, { translated: string }> {
constructor(props) {
super(props);
this.state = {
translated: /* @tr-ignore */ tr(props.message)
}
}
render() {
return this.state.translated;
}
}

View File

@ -1,662 +0,0 @@
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {manager, Sound} from "tc-shared/sound/Sounds";
import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {Settings, settings} from "tc-shared/settings";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import PermissionType from "tc-shared/permission/PermissionType";
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {openBanList} from "tc-shared/ui/modal/ModalBanList";
import {
add_current_server,
Bookmark,
bookmarks,
BookmarkType,
boorkmak_connect,
DirectoryBookmark
} from "tc-shared/bookmarks";
import {IconManager} from "tc-shared/FileManager";
import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage";
import {spawnPlaylistManage} from "tc-shared/ui/modal/ModalPlaylistList";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {formatMessage} from "tc-shared/ui/frames/chat";
import * as slog from "tc-shared/ui/frames/server_log";
import * as top_menu from "./MenuBar";
export let control_bar: ControlBar; /* global variable to access the control bar */
export function set_control_bar(bar: ControlBar) { control_bar = bar; }
export type MicrophoneState = "disabled" | "muted" | "enabled";
export type HeadphoneState = "muted" | "enabled";
export type AwayState = "away-global" | "away" | "online";
export class ControlBar {
private _button_away_active: AwayState;
private _button_microphone: MicrophoneState;
private _button_speakers: HeadphoneState;
private _button_subscribe_all: boolean;
private _button_query_visible: boolean;
private connection_handler: ConnectionHandler | undefined;
private _button_hostbanner: JQuery;
htmlTag: JQuery;
constructor(htmlTag: JQuery) {
this.htmlTag = htmlTag;
}
initialize_connection_handler_state(handler?: ConnectionHandler) {
/* setup the state like the last displayed one */
handler.client_status.output_muted = this._button_speakers === "muted";
handler.client_status.input_muted = this._button_microphone === "muted";
handler.client_status.channel_subscribe_all = this._button_subscribe_all;
handler.client_status.queries_visible = this._button_query_visible;
}
set_connection_handler(handler?: ConnectionHandler) {
if(this.connection_handler == handler)
return;
this.connection_handler = handler;
this.apply_server_state();
this.update_connection_state();
}
apply_server_state() {
if(!this.connection_handler)
return;
const flag_away = typeof(this.connection_handler.client_status.away) === "string" || this.connection_handler.client_status.away;
if(!flag_away)
this.button_away_active = "online";
else if(flag_away && this._button_away_active === "online")
this.button_away_active = "away";
this.button_query_visible = this.connection_handler.client_status.queries_visible;
this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all;
this.apply_server_hostbutton();
this.apply_server_voice_state();
}
apply_server_hostbutton() {
const server = this.connection_handler.channelTree.server;
if(server && server.properties.virtualserver_hostbutton_gfx_url) {
this._button_hostbanner
.attr("title", server.properties.virtualserver_hostbutton_tooltip || server.properties.virtualserver_hostbutton_gfx_url)
.attr("href", server.properties.virtualserver_hostbutton_url);
this._button_hostbanner.find("img").attr("src", server.properties.virtualserver_hostbutton_gfx_url);
this._button_hostbanner.each((_, e) => { e.style.display = null; });
} else {
this._button_hostbanner.each((_, e) => { e.style.display = "none"; });
}
}
apply_server_voice_state() {
if(!this.connection_handler)
return;
this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled";
this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled";
top_menu.update_state(); //TODO: Only run "small" update?
}
current_connection_handler() {
return this.connection_handler;
}
initialise() {
let dropdownify = (tag: JQuery) => {
tag.find(".dropdown-arrow").on('click', () => {
tag.addClass("displayed");
}).hover(() => {
tag.addClass("displayed");
}, () => {
if(tag.find(".dropdown:hover").length > 0)
return;
tag.removeClass("displayed");
});
tag.on('mouseleave', () => {
tag.removeClass("displayed");
});
};
this.htmlTag.find(".btn_connect").on('click', this.on_open_connect.bind(this));
this.htmlTag.find(".btn_connect_new_tab").on('click', this.on_open_connect_new_tab.bind(this));
this.htmlTag.find(".btn_disconnect").on('click', this.on_execute_disconnect.bind(this));
this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.bind(this));
this.htmlTag.find(".btn_mute_output").on('click', this.on_toggle_sound.bind(this));
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe.bind(this));
this.htmlTag.find(".btn_query_toggle").on('click', this.on_toggle_query_view.bind(this));
this.htmlTag.find(".btn_open_settings").on('click', this.on_open_settings.bind(this));
this.htmlTag.find(".btn_permissions").on('click', this.on_open_permissions.bind(this));
this.htmlTag.find(".btn_banlist").on('click', this.on_open_banslist.bind(this));
this.htmlTag.find(".button-playlist-manage").on('click', this.on_open_playlist_manage.bind(this));
this.htmlTag.find(".btn_token_use").on('click', this.on_token_use.bind(this));
this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.bind(this));
(this._button_hostbanner = this.htmlTag.find(".button-hostbutton")).hide().on('click', () => {
if(!this.connection_handler) return;
const server = this.connection_handler.channelTree.server;
if(!server || !server.properties.virtualserver_hostbutton_url) return;
window.open(server.properties.virtualserver_hostbutton_url, '_blank');
});
{
this.htmlTag.find(".btn_away_disable").on('click', this.on_away_disable.bind(this));
this.htmlTag.find(".btn_away_disable_global").on('click', this.on_away_disable_global.bind(this));
this.htmlTag.find(".btn_away_enable").on('click', this.on_away_enable.bind(this));
this.htmlTag.find(".btn_away_enable_global").on('click', this.on_away_enable_global.bind(this));
this.htmlTag.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
this.htmlTag.find(".btn_away_message_global").on('click', this.on_away_set_message_global.bind(this));
this.htmlTag.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
}
dropdownify(this.htmlTag.find(".container-connect"));
dropdownify(this.htmlTag.find(".container-disconnect"));
dropdownify(this.htmlTag.find(".btn_token"));
dropdownify(this.htmlTag.find(".btn_away"));
dropdownify(this.htmlTag.find(".btn_bookmark"));
dropdownify(this.htmlTag.find(".btn_query"));
dropdownify(this.htmlTag.find(".dropdown-audio"));
dropdownify(this.htmlTag.find(".dropdown-servertools"));
{
}
{
this.htmlTag.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
this.htmlTag.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
}
{
/* search for query buttons not only on the large device button */
this.htmlTag.find(".btn_query_create").on('click', this.on_open_query_create.bind(this));
this.htmlTag.find(".btn_query_manage").on('click', this.on_open_query_manage.bind(this));
}
this.update_bookmarks();
this.update_bookmark_status();
//Need an initialise
this.button_speaker = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) ? "muted" : "enabled";
this.button_microphone = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) ? "muted" : "enabled";
this.button_subscribe_all = true;
this.button_query_visible = false;
}
/* Update the UI */
set button_away_active(flag: AwayState) {
if(this._button_away_active === flag)
return;
this._button_away_active = flag;
this.update_button_away();
}
update_button_away() {
const button_away_enable = this.htmlTag.find(".btn_away_enable");
const button_away_disable = this.htmlTag.find(".btn_away_disable");
const button_away_toggle = this.htmlTag.find(".btn_away_toggle");
const button_away_disable_global = this.htmlTag.find(".btn_away_disable_global");
const button_away_enable_global = this.htmlTag.find(".btn_away_enable_global");
const button_away_message_global = this.htmlTag.find(".btn_away_message_global");
button_away_toggle.toggleClass("activated", this._button_away_active !== "online");
button_away_enable.toggle(this._button_away_active === "online");
button_away_disable.toggle(this._button_away_active !== "online");
const connections = server_connections.server_connection_handlers();
if(connections.length <= 1) {
button_away_disable_global.hide();
button_away_enable_global.hide();
button_away_message_global.hide();
} else {
button_away_message_global.show();
button_away_enable_global.toggle(server_connections.server_connection_handlers().filter(e => !e.client_status.away).length > 0);
button_away_disable_global.toggle(
this._button_away_active === "away-global" ||
server_connections.server_connection_handlers().filter(e => typeof(e.client_status.away) === "string" || e.client_status.away).length > 0
)
}
}
set button_microphone(state: MicrophoneState) {
if(this._button_microphone === state)
return;
this._button_microphone = state;
let tag = this.htmlTag.find(".btn_mute_input");
const tag_icon = tag.find(".icon_em, .icon");
tag.toggleClass('activated', state === "muted");
/*
tag_icon
.toggleClass('client-input_muted', state === "muted")
.toggleClass('client-capture', state === "enabled")
.toggleClass('client-activate_microphone', state === "disabled");
*/
tag_icon
.toggleClass('client-input_muted', state !== "disabled")
.toggleClass('client-capture', false)
.toggleClass('client-activate_microphone', state === "disabled");
if(state === "disabled")
tag_icon.attr('title', tr("Enable your microphone on this server"));
else if(state === "enabled")
tag_icon.attr('title', tr("Mute microphone"));
else
tag_icon.attr('title', tr("Unmute microphone"));
}
set button_speaker(state: HeadphoneState) {
if(this._button_speakers === state)
return;
this._button_speakers = state;
let tag = this.htmlTag.find(".btn_mute_output");
const tag_icon = tag.find(".icon_em, .icon");
tag.toggleClass('activated', state === "muted");
/*
tag_icon
.toggleClass('client-output_muted', state !== "enabled")
.toggleClass('client-volume', state === "enabled");
*/
tag_icon
.toggleClass('client-output_muted', true)
.toggleClass('client-volume', false);
if(state === "enabled")
tag_icon.attr('title', tr("Mute sound"));
else
tag_icon.attr('title', tr("Unmute sound"));
}
set button_subscribe_all(state: boolean) {
if(this._button_subscribe_all === state)
return;
this._button_subscribe_all = state;
this.htmlTag
.find(".button-subscribe-mode")
.toggleClass('activated', this._button_subscribe_all)
.find('.icon_em')
.toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all)
.toggleClass('client-subscribe_to_all_channels', this._button_subscribe_all);
}
set button_query_visible(state: boolean) {
if(this._button_query_visible === state)
return;
this._button_query_visible = state;
const button = this.htmlTag.find(".btn_query_toggle");
button.toggleClass('activated', this._button_query_visible);
if(this._button_query_visible)
button.find(".query-text").text(tr("Hide server queries"));
else
button.find(".query-text").text(tr("Show server queries"));
}
/* UI listener */
private on_away_toggle() {
if(this._button_away_active === "away" || this._button_away_active === "away-global")
this.button_away_active = "online";
else
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(this._button_away_active !== "online");
}
private on_away_enable() {
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(true);
}
private on_away_disable() {
this.button_away_active = "online";
if(this.connection_handler)
this.connection_handler.set_away_status(false);
}
private on_away_set_message() {
createInputModal(tr("Set away message"), tr("Please enter your away message"), message => true, message => {
if(typeof(message) === "string") {
this.button_away_active = "away";
if(this.connection_handler)
this.connection_handler.set_away_status(message);
}
}).open();
}
private on_away_enable_global() {
this.button_away_active = "away-global";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(true);
}
private on_away_disable_global() {
this.button_away_active = "online";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(false);
}
private on_away_set_message_global() {
createInputModal(tr("Set global away message"), tr("Please enter your global away message"), message => true, message => {
if(typeof(message) === "string") {
this.button_away_active = "away";
for(const connection of server_connections.server_connection_handlers())
connection.set_away_status(message);
}
}).open();
}
private on_toggle_microphone() {
if(this._button_microphone === "disabled" || this._button_microphone === "muted") {
this.button_microphone = "enabled";
manager.play(Sound.MICROPHONE_ACTIVATED);
} else {
this.button_microphone = "muted";
manager.play(Sound.MICROPHONE_MUTED);
}
if(this.connection_handler) {
this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled";
if(!this.connection_handler.client_status.input_hardware)
this.connection_handler.acquire_recorder(default_recorder, true); /* acquire_recorder already updates the voice status */
else
this.connection_handler.update_voice_status(undefined);
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this.connection_handler.client_status.input_muted)
}
}
private on_toggle_sound() {
if(this._button_speakers === "muted") {
this.button_speaker = "enabled";
manager.play(Sound.SOUND_ACTIVATED);
} else {
this.button_speaker = "muted";
manager.play(Sound.SOUND_MUTED);
}
if(this.connection_handler) {
this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled";
this.connection_handler.update_voice_status(undefined);
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this.connection_handler.client_status.output_muted)
}
}
private on_toggle_channel_subscribe() {
this.button_subscribe_all = !this._button_subscribe_all;
if(this.connection_handler) {
this.connection_handler.client_status.channel_subscribe_all = this._button_subscribe_all;
if(this._button_subscribe_all)
this.connection_handler.channelTree.subscribe_all_channels();
else
this.connection_handler.channelTree.unsubscribe_all_channels(true);
this.connection_handler.settings.changeServer(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, this._button_subscribe_all);
}
}
private on_toggle_query_view() {
this.button_query_visible = !this._button_query_visible;
if(this.connection_handler) {
this.connection_handler.client_status.queries_visible = this._button_query_visible;
this.connection_handler.channelTree.toggle_server_queries(this._button_query_visible);
this.connection_handler.settings.changeServer(Settings.KEY_CONTROL_SHOW_QUERIES, this._button_subscribe_all);
}
}
private on_open_settings() {
spawnSettingsModal();
}
private on_open_connect() {
if(this.connection_handler)
this.connection_handler.cancel_reconnect(true);
spawnConnectModal({}, {
url: "ts.TeaSpeak.de",
enforce: false
});
}
private on_open_connect_new_tab() {
spawnConnectModal({
default_connect_new_tab: true
}, {
url: "ts.TeaSpeak.de",
enforce: false
});
}
update_connection_state() {
if(this.connection_handler.serverConnection && this.connection_handler.serverConnection.connected()) {
this.htmlTag.find(".container-disconnect").show();
this.htmlTag.find(".container-connect").hide();
} else {
this.htmlTag.find(".container-disconnect").hide();
this.htmlTag.find(".container-connect").show();
}
/*
switch (this.connection_handler.serverConnection ? this.connection_handler.serverConnection.connected() : ConnectionState.UNCONNECTED) {
case ConnectionState.CONNECTED:
case ConnectionState.CONNECTING:
case ConnectionState.INITIALISING:
this.htmlTag.find(".container-disconnect").show();
this.htmlTag.find(".container-connect").hide();
break;
default:
this.htmlTag.find(".container-disconnect").hide();
this.htmlTag.find(".container-connect").show();
}
*/
}
private on_execute_disconnect() {
this.connection_handler.cancel_reconnect(true);
this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
this.update_connection_state();
this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED);
this.connection_handler.log.log(slog.Type.DISCONNECTED, {});
}
private on_token_use() {
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return;
if(this.connection_handler.serverConnection.connected)
this.connection_handler.serverConnection.send_command("tokenuse", {
token: result
}).then(() => {
createInfoModal(tr("Use token"), tr("Toke successfully used!")).open();
}).catch(error => {
//TODO tr
createErrorModal(tr("Use token"), formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open();
});
}).open();
}
private on_token_list() {
createErrorModal(tr("Not implemented"), tr("Token list is not implemented yet!")).open();
}
private on_open_permissions() {
let button = this.htmlTag.find(".btn_permissions");
button.addClass("activated");
setTimeout(() => {
if(this.connection_handler)
spawnPermissionEdit(this.connection_handler).open();
else
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
button.removeClass("activated");
}, 0);
}
private on_open_banslist() {
if(!this.connection_handler.serverConnection) return;
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
openBanList(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open();
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
}
private on_bookmark_server_add() {
add_current_server();
}
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, .directory").remove();
const build_entry = (bookmark: DirectoryBookmark | Bookmark) => {
if(bookmark.type == BookmarkType.ENTRY) {
const mark = <Bookmark>bookmark;
const bookmark_connect = (new_tab: boolean) => {
this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working
boorkmak_connect(mark, new_tab);
};
return $.spawn("div")
.addClass("bookmark")
.append(
//$.spawn("div").addClass("icon client-server")
IconManager.generate_tag(IconManager.load_cached_icon(mark.last_icon_id || 0), {animate: false}) /* must be false */
)
.append(
$.spawn("div")
.addClass("name")
.text(bookmark.display_name)
.on('click', event => {
if(event.isDefaultPrevented())
return;
bookmark_connect(false);
})
.on('contextmenu', event => {
if(event.isDefaultPrevented())
return;
event.preventDefault();
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect"),
icon_class: 'client-connect',
callback: () => bookmark_connect(false)
}, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect in a new tab"),
icon_class: 'client-connect',
callback: () => bookmark_connect(true),
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
}, contextmenu.Entry.CLOSE(() => {
setTimeout(() => {
this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show")
}, 250);
}));
this.htmlTag.find(".btn_bookmark.dropdown-arrow").addClass("force-show");
})
)
} else {
const mark = <DirectoryBookmark>bookmark;
const container = $.spawn("div").addClass("sub-menu dropdown");
const result = $.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)
);
/* we've to keep it this order because we're then keeping the reference of the loading icons... */
for(const member of mark.content)
container.append(build_entry(member));
return result;
}
};
for(const bookmark of bookmarks().content) {
const entry = build_entry(bookmark);
tag_bookmark.append(entry);
}
}
private on_bookmark_manage() {
spawnBookmarkModal();
}
private on_open_query_create() {
if(this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) {
spawnQueryCreate(this.connection_handler);
} else {
createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open();
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
}
}
private on_open_query_manage() {
if(this.connection_handler && this.connection_handler.connected) {
spawnQueryManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
}
}
private on_open_playlist_manage() {
if(this.connection_handler && this.connection_handler.connected) {
spawnPlaylistManage(this.connection_handler);
} else {
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
}
}
}

View File

@ -14,17 +14,17 @@ import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit"; import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {openBanList} from "tc-shared/ui/modal/ModalBanList"; import {openBanList} from "tc-shared/ui/modal/ModalBanList";
import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage"; import {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery"; import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
import {spawnAbout} from "tc-shared/ui/modal/ModalAbout"; import {spawnAbout} from "tc-shared/ui/modal/ModalAbout";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {formatMessage} from "tc-shared/ui/frames/chat"; import {formatMessage} from "tc-shared/ui/frames/chat";
import * as slog from "tc-shared/ui/frames/server_log"; import * as slog from "tc-shared/ui/frames/server_log";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export interface HRItem { } export interface HRItem { }
@ -348,7 +348,8 @@ export function initialize() {
handler.sound.play(Sound.CONNECTION_DISCONNECTED); handler.sound.play(Sound.CONNECTION_DISCONNECTED);
handler.log.log(slog.Type.DISCONNECTED, {}); handler.log.log(slog.Type.DISCONNECTED, {});
} }
control_bar.update_connection_state();
control_bar_instance()?.events().fire("update_state", { state: "connect-state" });
update_state(); update_state();
}; };
item = menu.append_item(tr("Disconnect from current server")); item = menu.append_item(tr("Disconnect from current server"));

View File

@ -1,7 +1,8 @@
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "./MenuBar"; import * as top_menu from "./MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
import {client_control_events} from "tc-shared/main";
export let server_connections: ServerConnectionManager; export let server_connections: ServerConnectionManager;
export function initialize(manager: ServerConnectionManager) { export function initialize(manager: ServerConnectionManager) {
@ -97,7 +98,7 @@ export class ServerConnectionManager {
handler.resize_elements(); handler.resize_elements();
} }
this.active_handler = handler; this.active_handler = handler;
control_bar.set_connection_handler(handler); client_control_events.fire("action_set_active_connection_handler", { handler: handler }); //FIXME: This even should set the new handler, not vice versa!
top_menu.update_state(); top_menu.update_state();
} }

View File

@ -0,0 +1,30 @@
import {Registry} from "tc-shared/events";
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/index";
import {manager, Sound} from "tc-shared/sound/Sounds";
function initialize_sounds(event_registry: Registry<ControlBarEvents>) {
{
let microphone_muted = undefined;
event_registry.on("update_microphone_state", event => {
if(microphone_muted === event.muted) return;
if(typeof microphone_muted !== "undefined")
manager.play(event.muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
microphone_muted = event.muted;
})
}
{
let speakers_muted = undefined;
event_registry.on("update_speaker_state", event => {
if(speakers_muted === event.muted) return;
if(typeof speakers_muted !== "undefined")
manager.play(event.muted ? Sound.SOUND_MUTED : Sound.SOUND_ACTIVATED);
speakers_muted = event.muted;
})
}
}
export = (event_registry: Registry<ControlBarEvents>) => {
initialize_sounds(event_registry);
};
//TODO: Left action handler!

View File

@ -0,0 +1,188 @@
/* Some general browser helpers */
/* border etc */
.button, .dropdownArrow {
text-align: center;
border: 0.05em solid rgba(0, 0, 0, 0);
border-radius: 0.1em;
background-color: #454545;
-moz-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
-o-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
-webkit-transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
transition: background-color 0.25s ease-in-out, border-color 0.25s ease-in-out;
}
.button:hover, .dropdownArrow:hover {
background-color: #393c43;
border-color: #4a4c55;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
}
.button.activated, .dropdownArrow.activated {
background-color: #2f3841;
border-color: #005fa1;
}
.button.activated:hover, .dropdownArrow.activated:hover {
background-color: #263340;
border-color: #005fa1;
}
.button.activated.theme-red, .dropdownArrow.activated.theme-red {
background-color: #412f2f;
border-color: #a10000;
}
.button.activated.theme-red:hover, .dropdownArrow.activated.theme-red:hover {
background-color: #402626;
border-color: #a10000;
}
.button :global(.icon_em), .dropdownArrow :global(.icon_em) {
font-size: 1.5em;
}
.button {
display: flex;
flex-direction: row;
justify-content: center;
height: 2em;
width: 2em;
cursor: pointer;
align-items: center;
margin-right: 5px;
margin-left: 5px;
}
.button.buttonHostbutton {
overflow: hidden;
padding: 0.25em;
}
.button.buttonHostbutton img {
min-width: 1.5em;
max-width: 1.5em;
height: 1.5em;
width: 1.5em;
}
.buttonDropdown {
height: 100%;
position: relative;
}
.buttonDropdown .buttons {
height: 2em;
align-items: center;
display: flex;
flex-direction: row;
}
.buttonDropdown .buttons .dropdownArrow {
height: 2em;
display: inline-flex;
justify-content: space-around;
width: 1.5em;
cursor: pointer;
border-radius: 0 0.1em 0.1em 0;
align-items: center;
border-left: 0;
}
.buttonDropdown .buttons .button {
margin-right: 0;
}
.buttonDropdown .buttons:hover .button, .buttonDropdown .buttons:hover .dropdownArrow {
background-color: #393c43;
border-color: #4a4c55;
}
.buttonDropdown .buttons:hover .button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.buttonDropdown .dropdown {
display: none;
position: absolute;
margin-left: 5px;
color: #c4c5c5;
background-color: #2d3032;
align-items: center;
border: 0.05em solid #2c2525;
border-radius: 0 0.15em 0.15em 0.15em;
width: 15em;
/* fallback */
width: max-content;
max-width: 25em;
z-index: 1000;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
}
.buttonDropdown .dropdown:global(.right) {
right: 0;
}
.buttonDropdown .dropdown :global .icon, .buttonDropdown .dropdown :global .icon-container, .buttonDropdown .dropdown :global .icon_em {
vertical-align: middle;
margin-right: 5px;
}
.buttonDropdown .dropdown :global .icon-empty, .buttonDropdown .dropdown :global .icon_empty {
flex-shrink: 0;
flex-grow: 0;
height: 16px;
width: 16px;
}
.buttonDropdown .dropdown .dropdownEntry {
position: relative;
display: flex;
flex-direction: row;
cursor: pointer;
padding: 1px 2px 1px 4px;
align-items: center;
justify-content: stretch;
}
.buttonDropdown .dropdown .dropdownEntry .entryName {
flex-grow: 1;
flex-shrink: 1;
vertical-align: text-top;
margin-right: 0.5em;
}
.buttonDropdown .dropdown .dropdownEntry .icon, .buttonDropdown .dropdown .dropdownEntry .arrow {
flex-grow: 0;
flex-shrink: 0;
}
.buttonDropdown .dropdown .dropdownEntry .arrow {
margin-right: 0.5em;
}
.buttonDropdown .dropdown .dropdownEntry:first-of-type {
border-radius: 0.1em 0.1em 0 0;
}
.buttonDropdown .dropdown .dropdownEntry:last-of-type {
border-radius: 0 0 0.1em 0.1em;
}
.buttonDropdown .dropdown .dropdownEntry > .dropdown {
margin-left: 0;
}
.buttonDropdown .dropdown .dropdownEntry:hover {
background-color: #252729;
}
.buttonDropdown .dropdown .dropdownEntry:hover > .dropdown {
display: block;
margin-left: 0;
left: 100%;
top: 0;
}
.buttonDropdown .dropdown.displayLeft {
margin-left: -179px;
border-radius: 0.15em 0 0.15em 0.15em;
}
.buttonDropdown.dropdownDisplayed > .dropdown {
display: block;
}
.buttonDropdown.dropdownDisplayed .button, .buttonDropdown.dropdownDisplayed .dropdown-arrow {
background-color: #393c43;
border-color: #4a4c55;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.buttonDropdown.dropdownDisplayed .button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.buttonDropdown hr {
margin-top: 5px;
margin-bottom: 5px;
}
.buttonBookmarks .dropdown {
width: 300px;
}
/*# sourceMappingURL=button.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../../../css/static/mixin.scss","button.scss","../../../../css/static/properties.scss"],"names":[],"mappings":"AAAA;ACKA;AACA;EACI;EAEA;EACA,eCLkB;EDOlB;EDTH,iBCqCG;EDpCH,eCoCG;EDnCH,oBCmCG;EDlCH,YCkCG;;AA1BA;EACI;EACA;AACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAOZ;EACI;;;AAIR;EACI;EACA;EACA;EAEA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EASI;EACA;;AATA;EACI;EACA;EAEA;EACA;;;AAQZ;EACI;EACA;;AAEA;EACI;EAEA;EAEA;EACA;;AAEA;EACI;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAGJ;EACI;;AAIA;EACI;EACA;;AAGJ;EACI;EAEA;EACA;;AAKZ;EACI;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EAEA;AAAa;EACb;EAEA;EAEA;AACA;;AAEA;EACI;;AAIA;EACI;EACA;;AAGJ;EACI;EACA;EAEA;EACA;;AAIR;EACI;EAEA;EACA;EACA;EACA;EAEA;EACA;;AAEA;EACI;EACA;EAEA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAEA;EACI;EACA;EAEA;EACA;;AAMZ;EACI;EACA;;AAKJ;EACI;;AAGJ;EACI;EACA;EAEA;EACA;;AAGJ;EACI;EAEA;EACA;;AAKR;EACI;EACA;;;AAKJ;EACI","file":"button.css"}

View File

@ -0,0 +1,252 @@
@import "../../../../css/static/properties";
@import "../../../../css/static/mixin";
$border_color_activated: rgba(255, 255, 255, .75);
/* border etc */
.button, .dropdownArrow {
text-align: center;
border: .05em solid rgba(0, 0, 0, 0);
border-radius: $border_radius_small;
background-color: #454545;
&:hover {
background-color: #393c43;
border-color: #4a4c55;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
}
&.activated {
background-color: #2f3841;
border-color: #005fa1;
&:hover {
background-color: #263340;
border-color: #005fa1;
}
&.theme-red {
background-color: #412f2f;
border-color: #a10000;
&:hover {
background-color: #402626;
border-color: #a10000;
}
}
}
@include transition(background-color $button_hover_animation_time ease-in-out, border-color $button_hover_animation_time ease-in-out);
:global(.icon_em) {
font-size: 1.5em;
}
}
.button {
display: flex;
flex-direction: row;
justify-content: center;
height: 2em;
width: 2em;
cursor: pointer;
align-items: center;
margin-right: 5px;
margin-left: 5px;
&.buttonHostbutton {
img {
min-width: 1.5em;
max-width: 1.5em;
height: 1.5em;
width: 1.5em;
}
overflow: hidden;
padding: .25em;
}
}
.buttonDropdown {
height: 100%;
position: relative;
.buttons {
height: 2em;
align-items: center;
display: flex;
flex-direction: row;
.dropdownArrow {
height: 2em;
display: inline-flex;
justify-content: space-around;
width: 1.5em;
cursor: pointer;
border-radius: 0 $border_radius_small $border_radius_small 0;
align-items: center;
border-left: 0;
}
.button {
margin-right: 0;
}
&:hover {
.button, .dropdownArrow {
background-color: #393c43;
border-color: #4a4c55;
}
.button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
}
}
.dropdown {
display: none;
position: absolute;
margin-left: 5px;
color: #c4c5c5;
background-color: #2d3032;
align-items: center;
border: .05em solid #2c2525;
border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
width: 15em; /* fallback */
width: max-content;
max-width: 25em;
z-index: 1000;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
&:global(.right) {
right: 0;
}
:global {
.icon, .icon-container, .icon_em {
vertical-align: middle;
margin-right: 5px;
}
.icon-empty, .icon_empty {
flex-shrink: 0;
flex-grow: 0;
height: 16px;
width: 16px;
}
}
.dropdownEntry {
position: relative;
display: flex;
flex-direction: row;
cursor: pointer;
padding: 1px 2px 1px 4px;
align-items: center;
justify-content: stretch;
.entryName {
flex-grow: 1;
flex-shrink: 1;
vertical-align: text-top;
margin-right: .5em;
}
.icon, .arrow {
flex-grow: 0;
flex-shrink: 0;
}
.arrow {
margin-right: .5em;
}
&:first-of-type {
border-radius: .1em .1em 0 0;
}
&:last-of-type {
border-radius: 0 0 .1em .1em;
}
> .dropdown {
margin-left: 0;
}
&:hover {
background-color: #252729;
> .dropdown {
display: block;
margin-left: 0;
left: 100%;
top: 0;
}
}
}
&.displayLeft {
margin-left: -179px;
border-radius: $border_radius_middle 0 $border_radius_middle $border_radius_middle;
}
}
&.dropdownDisplayed {
> .dropdown {
display: block;
}
.button, .dropdown-arrow {
background-color: #393c43;
border-color: #4a4c55;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.button {
border-right-color: transparent;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
}
hr {
margin-top: 5px;
margin-bottom: 5px;
}
}
.buttonBookmarks {
.dropdown {
width: 300px;
}
}

View File

@ -0,0 +1,86 @@
import * as React from "react";
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
import {DropdownContainer} from "tc-shared/ui/frames/control-bar/dropdown";
const cssStyle = require("./button.scss");
export interface ButtonState {
switched: boolean;
dropdownShowed: boolean;
dropdownForceShow: boolean;
}
export interface ButtonProperties {
colorTheme?: "red" | "default";
autoSwitch: boolean;
tooltip?: string;
iconNormal: string;
iconSwitched?: string;
onToggle?: (state: boolean) => boolean | void;
dropdownButtonExtraClass?: string;
switched?: boolean;
}
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
protected default_state(): ButtonState {
return {
switched: false,
dropdownShowed: false,
dropdownForceShow: false
}
}
render() {
const switched = this.props.switched || this.state.switched;
const buttonRootClass = this.classList(
cssStyle.button,
switched ? cssStyle.activated : "",
typeof this.props.colorTheme === "string" ? cssStyle["theme-" + this.props.colorTheme] : "");
const button = (
<div className={buttonRootClass} title={this.props.tooltip} onClick={this.onClick.bind(this)}>
<div className={this.classList("icon_em ", (switched ? this.props.iconSwitched : "") || this.props.iconNormal)} />
</div>
);
if(!this.hasChildren())
return button;
return (
<div className={this.classList(cssStyle.buttonDropdown, this.state.dropdownShowed || this.state.dropdownForceShow ? cssStyle.dropdownDisplayed : "", this.props.dropdownButtonExtraClass)} onMouseLeave={this.onMouseLeave.bind(this)}>
<div className={cssStyle.buttons}>
{button}
<div className={cssStyle.dropdownArrow} onMouseEnter={this.onMouseEnter.bind(this)}>
<div className={this.classList("arrow", "down")} />
</div>
</div>
<DropdownContainer>
{this.props.children}
</DropdownContainer>
</div>
);
}
private onMouseEnter() {
this.updateState({
dropdownShowed: true
});
}
private onMouseLeave() {
this.updateState({
dropdownShowed: false
});
}
private onClick() {
const new_state = !this.state.switched;
const result = this.props.onToggle?.call(undefined, new_state);
if(this.props.autoSwitch)
this.updateState({ switched: typeof result === "boolean" ? result : new_state });
}
}

View File

@ -0,0 +1,84 @@
import * as React from "react";
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
const cssStyle = require("./button.scss");
export interface DropdownEntryProperties {
icon?: string | JQuery<HTMLDivElement>;
text: JSX.Element | string;
onClick?: (event) => void;
onContextMenu?: (event) => void;
}
class IconRenderer extends React.Component<{ icon: string | JQuery<HTMLDivElement> }, {}> {
private readonly icon_ref: React.RefObject<HTMLDivElement>;
constructor(props) {
super(props);
if(typeof this.props.icon === "object")
this.icon_ref = React.createRef();
}
render() {
if(!this.props.icon)
return <div className={"icon-container icon-empty"} />;
else if(typeof this.props.icon === "string")
return <div className={"icon " + this.props.icon} />;
return <div ref={this.icon_ref} />;
}
componentDidMount(): void {
if(this.icon_ref)
$(this.icon_ref.current).replaceWith(this.props.icon);
}
componentWillUnmount(): void {
if(this.icon_ref)
$(this.icon_ref.current).empty();
}
}
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
protected default_state() { return {}; }
render() {
if(this.props.children) {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
<IconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
<div className={this.classList("arrow", "right")} />
<DropdownContainer>
{this.props.children}
</DropdownContainer>
</div>
);
} else {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
<IconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
</div>
);
}
}
}
export interface DropdownContainerProperties { }
export interface DropdownContainerState { }
export class DropdownContainer extends ReactComponentBase<DropdownContainerProperties, DropdownContainerState> {
protected default_state() {
return { };
}
render() {
return (
<div className={this.classList(cssStyle.dropdown)}>
{this.props.children}
</div>
);
}
}

View File

@ -0,0 +1,28 @@
/* Some general browser helpers */
/* max height is 2em */
.controlBar {
display: flex;
flex-direction: row;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height: 100%;
align-items: center;
/* tmp fix for ultra small devices */
overflow-y: visible;
}
.controlBar .divider {
flex-grow: 0;
flex-shrink: 0;
border-left: 2px solid #393838;
height: calc(100% - 3px);
margin: 3px;
}
.controlBar .spacer {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
/*# sourceMappingURL=index.css.map */

View File

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../../../css/static/mixin.scss","index.scss"],"names":[],"mappings":"AAAA;ACGA;AACA;EACI;EACA;EDwDH,qBCtDwB;EDuDxB,kBCvDwB;EDwDxB,iBCxDwB;EDyDxB,aCzDwB;EAErB;EACA;AAEA;EACA;;AAEA;EACI;EACA;EAEA;EACA;EACA;;AAGJ;EACI;EACA;EAEA","file":"index.css"}

View File

@ -0,0 +1,32 @@
@import "../../../../css/static/properties";
@import "../../../../css/static/mixin";
/* max height is 2em */
.controlBar {
display: flex;
flex-direction: row;
@include user-select(none);
height: 100%;
align-items: center;
/* tmp fix for ultra small devices */
overflow-y: visible;
.divider {
flex-grow: 0;
flex-shrink: 0;
border-left:2px solid #393838;
height: calc(100% - 3px);
margin: 3px;
}
.spacer {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
}

View File

@ -0,0 +1,578 @@
import * as React from "react";
import {Button} from "./button";
import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown";
import {Translatable} from "tc-shared/ui/elements/i18n";
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {Settings, settings} from "tc-shared/settings";
import {
Bookmark,
bookmarks,
BookmarkType,
boorkmak_connect,
DirectoryBookmark,
find_bookmark
} from "tc-shared/bookmarks";
import {IconManager} from "tc-shared/FileManager";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {client_control_events} from "tc-shared/main";
const register_actions = require("./actions");
const cssStyle = require("./index.scss");
const cssButtonStyle = require("./button.scss");
export interface ConnectionState {
connected: boolean;
connectedAnywhere: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<ControlBarEvents> }, ConnectionState> {
protected default_state(): ConnectionState {
return {
connected: false,
connectedAnywhere: false
}
}
render() {
let subentries = [];
if(this.props.multiSession) {
if(!this.state.connected) {
subentries.push(
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable message={"Connect to a server"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: false }) } />
);
} else {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: false }) }/>
);
}
if(this.state.connectedAnywhere) {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: true }) }/>
);
}
subentries.push(
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable message={"Connect to a server in another tab"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: true }) } />
);
}
if(!this.state.connected) {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}
onToggle={ () => client_control_events.fire("action_open_connect", { new_tab: false }) }>
{subentries}
</Button>
);
} else {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}
onToggle={ () => client_control_events.fire("action_disconnect", { globally: false }) }>
{subentries}
</Button>
);
}
}
@EventHandler<ControlBarEvents>("update_connect_state")
private handleStateUpdate(state: ConnectionState) {
this.updateState(state);
}
}
@ReactEventHandler(obj => obj.props.event_registry)
class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, {}> {
private button_ref: React.RefObject<Button>;
protected initialize() {
this.button_ref = React.createRef();
}
protected default_state() {
return {};
}
render() {
const marks = bookmarks().content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e));
if(marks.length)
marks.splice(0, 0, <hr key={"hr"} />);
return (
<Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "bookmark-manage" })} />
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable message={"Add current server to bookmarks"} />} />
{marks}
</Button>
)
}
private renderBookmark(bookmark: Bookmark) {
return (
<DropdownEntry key={bookmark.unique_id}
icon={IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false})}
text={bookmark.display_name}
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
onContextMenu={this.onBookmarkContextMenu.bind(this, bookmark.unique_id)}/>
);
}
private renderDirectory(directory: DirectoryBookmark) {
return (
<DropdownEntry key={directory.unique_id} text={directory.display_name} >
{directory.content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e))}
</DropdownEntry>
)
}
private static onBookmarkClick(bookmark_id: string) {
const bookmark = find_bookmark(bookmark_id) as Bookmark;
if(!bookmark) return;
boorkmak_connect(bookmark, false);
}
private onBookmarkContextMenu(bookmark_id: string, event: MouseEvent) {
event.preventDefault();
const bookmark = find_bookmark(bookmark_id) as Bookmark;
if(!bookmark) return;
this.button_ref.current?.updateState({ dropdownForceShow: true });
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect"),
icon_class: 'client-connect',
callback: () => boorkmak_connect(bookmark, false)
}, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Connect in a new tab"),
icon_class: 'client-connect',
callback: () => boorkmak_connect(bookmark, true),
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
}, contextmenu.Entry.CLOSE(() => {
this.button_ref.current?.updateState({ dropdownForceShow: false });
}));
}
@EventHandler<ControlBarEvents>("update_bookmarks")
private handleStateUpdate() {
this.forceUpdate();
}
}
export interface AwayState {
away: boolean;
awayAnywhere: boolean;
awayAll: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, AwayState> {
protected default_state(): AwayState {
return {
away: false,
awayAnywhere: false,
awayAll: false
};
}
render() {
let dropdowns = [];
if(this.state.away) {
dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable message={"Go online"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: false })} />);
} else {
dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable message={"Set away on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable message={"Set away message on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: true })} />);
dropdowns.push(<hr key={"-hr"} />);
if(this.state.awayAnywhere) {
dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable message={"Go online for all servers"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: true })} />);
}
if(!this.state.awayAll) {
dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable message={"Set away on all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sama"} icon={"client-away"} text={<Translatable message={"Set away message for all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: true })} />);
/* switchable because we're switching it manually */
return (
<Button autoSwitch={false} iconNormal={this.state.away ? "client-present" : "client-away"}>
{dropdowns}
</Button>
);
}
@EventHandler<ControlBarEvents>("update_away_state")
private handleStateUpdate(state: AwayState) {
this.updateState(state);
}
}
export interface ChannelSubscribeState {
subscribeEnabled: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, ChannelSubscribeState> {
protected default_state(): ChannelSubscribeState {
return { subscribeEnabled: false };
}
render() {
return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"}
onToggle={flag => client_control_events.fire("action_set_channel_subscribe_mode", { subscribe: flag })}/>;
}
@EventHandler<ControlBarEvents>("update_subscribe_state")
private handleStateUpdate(state: ChannelSubscribeState) {
this.updateState(state);
}
}
export interface MicrophoneState {
enabled: boolean;
muted: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, MicrophoneState> {
protected default_state(): MicrophoneState {
return {
enabled: false,
muted: false
};
}
render() {
if(!this.state.enabled)
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />;
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: false })} />;
}
@EventHandler<ControlBarEvents>("update_microphone_state")
private handleStateUpdate(state: MicrophoneState) {
this.updateState(state);
}
}
export interface SpeakerState {
muted: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, SpeakerState> {
protected default_state(): SpeakerState {
return {
muted: false
};
}
render() {
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: true })}/>;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: false })}/>;
}
@EventHandler<ControlBarEvents>("update_speaker_state")
private handleStateUpdate(state: SpeakerState) {
this.updateState(state);
}
}
export interface QueryState {
queryShown: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class QueryButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, QueryState> {
protected default_state() {
return {
queryShown: false
};
}
render() {
let toggle;
if(this.state.queryShown)
toggle = <DropdownEntry icon={""} text={<Translatable message={"Hide server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: false })}/>;
else
toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Show server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: true })}/>;
return (
<Button switched={this.state.queryShown} autoSwitch={false} iconNormal={"client-server_query"}
onToggle={flag => client_control_events.fire("action_toggle_query", { shown: flag })}>
{toggle}
<DropdownEntry icon={"client-server_query"} text={<Translatable message={"Manage server queries"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "query-manage" })}/>
</Button>
)
}
@EventHandler<ControlBarEvents>("update_query_state")
private handleStateUpdate(state: QueryState) {
this.updateState(state);
}
}
export interface HostButtonState {
url?: string;
title?: string;
target_url?: string;
}
@ReactEventHandler(obj => obj.props.event_registry)
class HostButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, HostButtonState> {
protected default_state() {
return {
url: undefined,
target_url: undefined
};
}
render() {
if(!this.state.url)
return null;
return (
<a
className={this.classList(cssButtonStyle.button, cssButtonStyle.buttonHostbutton)}
title={this.state.title || tr("Hostbutton")}
href={this.state.target_url || this.state.url}
target={"_blank"} /* just to ensure */
onClick={this.onClick.bind(this)}>
<img alt={tr("Hostbutton")} src={this.state.url} />
</a>
);
}
private onClick(event: MouseEvent) {
window.open(this.state.target_url || this.state.url, '_blank');
event.preventDefault();
}
@EventHandler<ControlBarEvents>("update_host_button")
private handleStateUpdate(state: HostButtonState) {
this.updateState(state);
}
}
export interface ControlBarProperties {
multiSession: boolean;
}
@ReactEventHandler<ControlBar>(obj => obj.event_registry)
export class ControlBar extends React.Component<ControlBarProperties, {}> {
private readonly event_registry: Registry<ControlBarEvents>;
private connection: ConnectionHandler;
constructor(props) {
super(props);
this.event_registry = new Registry<ControlBarEvents>();
this.event_registry.enable_debug("control-bar");
register_actions(this.event_registry);
}
componentDidMount(): void {
}
/*
initialize_connection_handler_state(handler?: ConnectionHandler) {
handler.client_status.output_muted = this._button_speakers === "muted";
handler.client_status.input_muted = this._button_microphone === "muted";
handler.client_status.channel_subscribe_all = this._button_subscribe_all;
handler.client_status.queries_visible = this._button_query_visible;
}
*/
events() : Registry<ControlBarEvents> { return this.event_registry; }
render() {
return (
<div className={cssStyle.controlBar}>
<ConnectButton event_registry={this.event_registry} multiSession={this.props.multiSession} />
<BookmarkButton event_registry={this.event_registry} />
<div className={cssStyle.divider} />
<AwayButton event_registry={this.event_registry} />
<MicrophoneButton event_registry={this.event_registry} />
<SpeakerButton event_registry={this.event_registry} />
<div className={cssStyle.divider} />
<ChannelSubscribeButton event_registry={this.event_registry} />
<QueryButton event_registry={this.event_registry} />
<div className={cssStyle.spacer} />
<HostButton event_registry={this.event_registry} />
</div>
)
}
@EventHandler<ControlBarEvents>("set_connection_handler")
private handleSetConnectionHandler(event: ControlBarEvents["set_connection_handler"]) {
if(this.connection == event.handler) return;
this.connection = event.handler;
this.event_registry.fire("update_state_all");
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateHostButton(event: Event<ControlBarEvents>) {
if(event.type === "update_state")4
if(event.as<"update_state">().state !== "host-button")
return;
const sprops = this.connection?.channelTree.server?.properties;
if(!sprops || !sprops.virtualserver_hostbutton_gfx_url) {
this.event_registry.fire("update_host_button", {
url: undefined,
target_url: undefined,
title: undefined
});
return;
}
this.event_registry.fire("update_host_button", {
url: sprops.virtualserver_hostbutton_gfx_url,
target_url: sprops.virtualserver_hostbutton_url,
title: sprops.virtualserver_hostbutton_tooltip
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "subscribe-mode")
return;
this.event_registry.fire("update_subscribe_state", {
subscribeEnabled: !!this.connection?.client_status.channel_subscribe_all
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "connect-state")
return;
this.event_registry.fire("update_connect_state", {
connectedAnywhere: server_connections.server_connection_handlers().findIndex(e => e.connected) !== -1,
connected: !!this.connection?.connected
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "away")
return;
const connections = server_connections.server_connection_handlers();
const away_connections = server_connections.server_connection_handlers().filter(e => e.client_status.away);
const away_status = this.connection?.client_status.away;
this.event_registry.fire("update_away_state", {
awayAnywhere: away_connections.length > 0,
away: typeof away_status === "string" ? true : !!away_status,
awayAll: connections.length === away_connections.length
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "microphone")
return;
this.event_registry.fire("update_microphone_state", {
enabled: !!this.connection?.client_status.input_hardware,
muted: this.connection?.client_status.input_muted
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "speaker")
return;
this.event_registry.fire("update_speaker_state", {
muted: this.connection?.client_status.output_muted
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "query")
return;
this.event_registry.fire("update_query_state", {
queryShown: !!this.connection?.client_status.queries_visible
});
}
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<ControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "bookmarks")
return;
this.event_registry.fire("update_bookmarks");
}
}
let react_reference_: React.RefObject<ControlBar>;
export function react_reference() { return react_reference_ || (react_reference_ = React.createRef()); }
export function control_bar_instance() : ControlBar | undefined {
return react_reference_?.current;
}
export interface ControlBarEvents {
/* update the UI */
update_host_button: HostButtonState;
update_subscribe_state: ChannelSubscribeState;
update_connect_state: ConnectionState;
update_away_state: AwayState;
update_microphone_state: MicrophoneState;
update_speaker_state: SpeakerState;
update_query_state: QueryState;
update_bookmarks: {},
update_state: {
state: "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"
},
update_state_all: { },
/* trigger actions */
set_connection_handler: {
handler?: ConnectionHandler
},
server_updated: {
handler: ConnectionHandler,
category: "audio" | "settings-initialized" | "connection-state" | "away-status" | "hostbanner"
}
//settings-initialized: Update query and channel flags
}

View File

@ -4,6 +4,7 @@ import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
import * as htmltags from "tc-shared/ui/htmltags"; import * as htmltags from "tc-shared/ui/htmltags";
import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat"; import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat";
import {formatDate} from "tc-shared/MessageFormatter"; import {formatDate} from "tc-shared/MessageFormatter";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
export enum Type { export enum Type {
CONNECTION_BEGIN = "connection_begin", CONNECTION_BEGIN = "connection_begin",
@ -261,10 +262,13 @@ export type MessageBuilderOptions = {};
export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = { export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
"error_custom": (data: event.ErrorCustom, options) => { "error_custom": (data: event.ErrorCustom) => {
return [$.spawn("div").addClass("log-error").text(data.message)] return [$.spawn("div").addClass("log-error").text(data.message)]
} }
}; };
function register_message_builder<T extends keyof TypeInfo>(key: T, builder: MessageBuilder<T>) {
MessageBuilders[key] = builder;
}
export class ServerLog { export class ServerLog {
private readonly handle: ConnectionHandler; private readonly handle: ConnectionHandler;
@ -384,27 +388,27 @@ const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.genera
add_braces: braces add_braces: braces
}); });
MessageBuilders["connection_begin"] = (data: event.ConnectBegin, options) => { MessageBuilders["connection_begin"] = (data: event.ConnectBegin) => {
return formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); return formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port));
}; };
MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => formatMessage(tr("Resolving hostname")); MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve) => formatMessage(tr("Resolving hostname"));
MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved, options) => formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved) => formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port);
MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message);
MessageBuilders["connection_login"] = (data: event.ConnectionLogin, options) => formatMessage(tr("Logging in...")); MessageBuilders["connection_login"] = () => formatMessage(tr("Logging in..."));
MessageBuilders["connection_failed"] = (data: event.ConnectionFailed, options) => formatMessage(tr("Connect failed.")); MessageBuilders["connection_failed"] = () => formatMessage(tr("Connect failed."));
MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); MessageBuilders["connection_connected"] = (data: event.ConnectionConnected) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true));
MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed, options) => { MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed) => {
return formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); return formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no"));
}; };
MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { MessageBuilders["error_permission"] = (data: event.ErrorPermission) => {
return formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); return formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error"));
}; };
MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { MessageBuilders["client_view_enter"] = (data: event.ClientEnter) => {
if(data.reason == ViewReasonId.VREASON_SYSTEM) { if(data.reason == ViewReasonId.VREASON_SYSTEM) {
return undefined; return undefined;
} if(data.reason == ViewReasonId.VREASON_USER_ACTION) { } if(data.reason == ViewReasonId.VREASON_USER_ACTION) {
@ -450,7 +454,7 @@ MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => {
return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")];
}; };
MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => { MessageBuilders["client_view_move"] = (data: event.ClientMove) => {
if(data.reason == ViewReasonId.VREASON_MOVED) { if(data.reason == ViewReasonId.VREASON_MOVED) {
return formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), return formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"),
client_tag(data.client), client_tag(data.client),
@ -476,7 +480,7 @@ MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => {
return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")];
}; };
MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => { MessageBuilders["client_view_leave"] = (data: event.ClientLeave) => {
if(data.reason == ViewReasonId.VREASON_USER_ACTION) { if(data.reason == ViewReasonId.VREASON_USER_ACTION) {
return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to));
} else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) {
@ -509,41 +513,45 @@ MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => {
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")];
}; };
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => {
return bbcode_chat("[color=green]" + data.message + "[/color]"); return bbcode_chat("[color=green]" + data.message + "[/color]");
}; };
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { MessageBuilders["server_host_message"] = (data: event.WelcomeMessage) => {
return bbcode_chat("[color=green]" + data.message + "[/color]"); return bbcode_chat("[color=green]" + data.message + "[/color]");
}; };
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => {
if(data.own_client) { if(data.own_client) {
return formatMessage(tr("Nickname successfully changed.")); return tra("Nickname successfully changed.");
} else { } else {
return formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); return tra("{0} changed his nickname from \"{1}\" to \"{2}\"", client_tag(data.client), data.old_name, data.new_name);
} }
}; };
MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { register_message_builder("client_nickname_change_failed", (data) => {
return tra("Failed to change own client name: {}", data.reason);
});
MessageBuilders["global_message"] = () => {
return []; /* we do not show global messages within log */ return []; /* we do not show global messages within log */
}; };
MessageBuilders["disconnected"] = () => formatMessage(tr("Disconnected from server")); MessageBuilders["disconnected"] = () => formatMessage(tr("Disconnected from server"));
MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled, options) => { MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled) => {
return tra("Reconnecting in {0}.", format_time(data.timeout, tr("now"))) return tra("Reconnecting in {0}.", format_time(data.timeout, tr("now")))
}; };
MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => { MessageBuilders["reconnect_canceled"] = () => {
return tra("Canceled reconnect.") return tra("Canceled reconnect.")
}; };
MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => { MessageBuilders["reconnect_execute"] = () => {
return tra("Reconnecting...") return tra("Reconnecting...")
}; };
MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => { MessageBuilders["server_banned"] = (data: event.ServerBanned) => {
let result: JQuery[]; let result: JQuery[];
const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second")); const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second"));
@ -560,4 +568,39 @@ MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => {
} }
return result.map(e => e.addClass("log-error")); return result.map(e => e.addClass("log-error"));
}; };
register_message_builder("server_host_message_disconnect", (data) => {
return tra(data.message);
});
register_message_builder("server_requires_password", () => {
return tra("Server requires a password to connect.");
});
register_message_builder("server_closed", (data) => {
return data.message ? tra("Server has been closed ({}).", data.message) : tra("Server has been closed.");
});
register_message_builder("connection_command_error", (data) => {
let error_message;
if(typeof data.error === "string")
error_message = data.error;
else if(data.error instanceof CommandResult)
error_message = data.error.extra_message || data.error.message;
else
error_message = data.error + "";
return tra("Command execution resulted in: {}", error_message);
});
register_message_builder("channel_create", (data) => {
if(data.own_action)
return tra("Channel {} has been created.", channel_tag(data.channel));
return tra("Channel {} has been created by {}.", channel_tag(data.channel), client_tag(data.creator));
});
register_message_builder("channel_delete", (data) => {
if(data.own_action)
return tra("Channel {} has been deleted.", channel_tag(data.channel));
return tra("Channel {] has been deleted by {}.", channel_tag(data.channel), client_tag(data.deleter));
});

View File

@ -3,7 +3,7 @@ import {format} from "tc-shared/ui/frames/side/chat_helper";
import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat"; import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
import {LogCategory} from "tc-shared/log"; import {LogCategory} from "tc-shared/log";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {ChatBox} from "tc-shared/ui/frames/side/chat_box"; import {ChatBox} from "tc-shared/ui/frames/side/chat_box";
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame"; import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal} from "tc-shared/ui/elements/Modal";

View File

@ -66,7 +66,7 @@ export class MusicInfo {
destroy() { destroy() {
this.set_current_bot(undefined); this.set_current_bot(undefined);
this.events.destory(); this.events.destroy();
this._html_tag && this._html_tag.remove(); this._html_tag && this._html_tag.remove();
this._html_tag = undefined; this._html_tag = undefined;

View File

@ -4,6 +4,7 @@ import {ChannelEntry} from "tc-shared/ui/channel";
import {ClientEntry} from "tc-shared/ui/client"; import {ClientEntry} from "tc-shared/ui/client";
import {htmlEscape} from "tc-shared/ui/frames/chat"; import {htmlEscape} from "tc-shared/ui/frames/chat";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {guid} from "tc-shared/crypto/uid";
let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0}; let mouse_coordinates: {x: number, y: number} = {x: 0, y: 0};
@ -30,6 +31,7 @@ export interface ChannelProperties {
add_braces?: boolean add_braces?: boolean
} }
const callback_object_id = guid();
/* required for the bbcodes */ /* required for the bbcodes */
function generate_client_open(properties: ClientProperties) : string { function generate_client_open(properties: ClientProperties) : string {
let result = ""; let result = "";
@ -57,7 +59,7 @@ function generate_client_open(properties: ClientProperties) : string {
} }
/* add the click handler */ /* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'"; result += "oncontextmenu='return window[\"" + callback_object_id + "\"].callback_context_client($(this));'";
result = result + ">"; result = result + ">";
return result; return result;
@ -100,7 +102,7 @@ function generate_channel_open(properties: ChannelProperties) : string {
result = result + "channel-name='" + encodeURIComponent(properties.channel_name) + "' "; result = result + "channel-name='" + encodeURIComponent(properties.channel_name) + "' ";
/* add the click handler */ /* add the click handler */
result += "oncontextmenu='return htmltags.callbacks.callback_context_channel($(this));'"; result += "oncontextmenu='return window[\"" + callback_object_id + "\"].callback_context_channel($(this));'";
result = result + ">"; result = result + ">";
return result; return result;
@ -186,6 +188,7 @@ export namespace callbacks {
return false; return false;
} }
} }
window[callback_object_id] = callbacks;
declare const xbbcode; declare const xbbcode;
namespace bbcodes { namespace bbcodes {

View File

@ -16,8 +16,8 @@ import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import * as i18nc from "tc-shared/i18n/country"; import * as i18nc from "tc-shared/i18n/country";
import {formatMessage} from "tc-shared/ui/frames/chat"; import {formatMessage} from "tc-shared/ui/frames/chat";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "../frames/MenuBar"; import * as top_menu from "../frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export function spawnBookmarkModal() { export function spawnBookmarkModal() {
let modal: Modal; let modal: Modal;
@ -375,7 +375,7 @@ export function spawnBookmarkModal() {
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks"); modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
modal.close_listener.push(() => { modal.close_listener.push(() => {
control_bar.update_bookmarks(); control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
top_menu.rebuild_bookmarks(); top_menu.rebuild_bookmarks();
}); });

View File

@ -1,7 +1,6 @@
import {createModal, Modal} from "tc-shared/ui/elements/Modal"; import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {tra} from "tc-shared/i18n/localize"; import {tra} from "tc-shared/i18n/localize";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import * as loader from "tc-loader";
import { modal as emodal } from "tc-shared/events"; import { modal as emodal } from "tc-shared/events";
import {modal_settings} from "tc-shared/ui/modal/ModalSettings"; import {modal_settings} from "tc-shared/ui/modal/ModalSettings";
import {profiles} from "tc-shared/profiles/ConnectionProfile"; import {profiles} from "tc-shared/profiles/ConnectionProfile";

View File

@ -198,7 +198,7 @@ function settings_general_language(container: JQuery, modal: Modal) {
}; };
const update_current_selected = () => { const update_current_selected = () => {
const container_current = container.find(".selected-language6"); const container_current = container.find(".selected-language");
container_current.empty().text(tr("Loading")); container_current.empty().text(tr("Loading"));
let current_translation: RepositoryTranslation; let current_translation: RepositoryTranslation;
@ -1782,7 +1782,7 @@ export namespace modal_settings {
console.debug(tr("Changed default microphone device")); console.debug(tr("Changed default microphone device"));
event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id });
}).catch((error) => { }).catch((error) => {
log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error) log.warn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.unique_id : "none", error);
event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id }); event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id });
}); });
}); });
@ -1959,7 +1959,7 @@ export namespace modal_settings {
const tags = volume_bar_tags[device.device_id]; const tags = volume_bar_tags[device.device_id];
if(!tags) continue; if(!tags) continue;
let level = typeof device.level === "number" ? device.level : 100; let level = typeof device.level === "number" ? device.level : 0;
if(level > 100) level = 100; if(level > 100) level = 100;
else if(level < 0) level = 0; else if(level < 0) level = 0;
tags.error.attr('title', device.error || null).text(device.error || null); tags.error.attr('title', device.error || null).text(device.error || null);

View File

@ -1,7 +1,7 @@
import { import {
PermissionManager, PermissionManager,
} from "tc-shared/permission/PermissionManager"; } from "tc-shared/permission/PermissionManager";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal";
import {HTMLPermissionEditor} from "tc-shared/ui/modal/permission/HTMLPermissionEditor"; import {HTMLPermissionEditor} from "tc-shared/ui/modal/permission/HTMLPermissionEditor";

View File

@ -11,9 +11,9 @@ import {createServerModal} from "tc-shared/ui/modal/ModalServerEdit";
import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect"; import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect";
import {spawnAvatarList} from "tc-shared/ui/modal/ModalAvatarList"; import {spawnAvatarList} from "tc-shared/ui/modal/ModalAvatarList";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import {connection_log} from "tc-shared/ui/modal/ModalConnect"; import {connection_log} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./frames/MenuBar"; import * as top_menu from "./frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export class ServerProperties { export class ServerProperties {
virtualserver_host: string = ""; virtualserver_host: string = "";
@ -318,7 +318,8 @@ export class ServerEntry {
}); });
bookmarks.save_bookmark(); bookmarks.save_bookmark();
top_menu.rebuild_bookmarks(); top_menu.rebuild_bookmarks();
control_bar.update_bookmarks();
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
} }
if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons) if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons)
@ -332,8 +333,7 @@ export class ServerEntry {
if(update_bannner) if(update_bannner)
this.channelTree.client.hostbanner.update(); this.channelTree.client.hostbanner.update();
if(update_button) if(update_button)
if(control_bar.current_connection_handler() === this.channelTree.client) control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" });
control_bar.apply_server_hostbutton();
group.end(); group.end();
if(is_self_notify && this.info_request_promise_resolve) { if(is_self_notify && this.info_request_promise_resolve) {

View File

@ -1,7 +1,7 @@
import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import PermissionType from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {LogCategory} from "tc-shared/log"; import {LogCategory} from "tc-shared/log";
import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; import {KeyCode, SpecialKey} from "tc-shared/PPTListener";
import {createInputModal} from "tc-shared/ui/elements/Modal"; import {createInputModal} from "tc-shared/ui/elements/Modal";

View File

@ -46,7 +46,7 @@ export class RecorderProfile {
current_handler: ConnectionHandler; current_handler: ConnectionHandler;
callback_support_change: () => any; callback_input_change: (old_input: AbstractInput, new_input: AbstractInput) => Promise<void>;
callback_start: () => any; callback_start: () => any;
callback_stop: () => any; callback_stop: () => any;
@ -107,6 +107,9 @@ export class RecorderProfile {
if(this.callback_stop) if(this.callback_stop)
this.callback_stop(); this.callback_stop();
}; };
//TODO: Await etc?
this.callback_input_change && this.callback_input_change(undefined, this.input);
} }
private async load() { private async load() {
@ -199,6 +202,7 @@ export class RecorderProfile {
} }
} }
this.callback_input_change = undefined;
this.callback_start = undefined; this.callback_start = undefined;
this.callback_stop = undefined; this.callback_stop = undefined;
this.callback_unmount = undefined; this.callback_unmount = undefined;

View File

@ -3,7 +3,7 @@
"baseUrl": ".", "baseUrl": ".",
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs", "module": "commonjs",
"lib": ["es6", "dom"], "lib": ["es6", "dom"]
/* /*
"typeRoots": [], "typeRoots": [],

View File

@ -14,6 +14,6 @@
"compiler.ts", "compiler.ts",
"jsrender_generator.ts", "jsrender_generator.ts",
"ts_generator.ts", "ts_generator.ts",
"ttsc_transformer.ts" //"ttsc_transformer.ts"
] ]
} }

View File

@ -1,6 +1,7 @@
/* General file with least possible errors. This is just for your IDE (PHP-Storm for example) */ /* General file with least possible errors. This is just for your IDE (PHP-Storm for example) */
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"target": "es6", "target": "es6",
"module": "commonjs", "module": "commonjs",
"sourceMap": true, "sourceMap": true,

View File

@ -458,11 +458,14 @@ class JavascriptInput implements AbstractInput {
audio_constrains.groupId = group_id; audio_constrains.groupId = group_id;
audio_constrains.echoCancellation = true; audio_constrains.echoCancellation = true;
/* may supported */ (audio_constrains as any).autoGainControl = true; audio_constrains.autoGainControl = true;
/* may supported */ (audio_constrains as any).noiseSuppression = true; audio_constrains.noiseSuppression = true;
/* disabled because most the time we get a OverconstrainedError */ //audio_constrains.sampleSize = {min: 420, max: 960 * 10, ideal: 960}; /* disabled because most the time we get a OverconstrainedError */ //audio_constrains.sampleSize = {min: 420, max: 960 * 10, ideal: 960};
const stream = await media_function({audio: audio_constrains, video: undefined}); const stream = await media_function({
audio: audio_constrains,
video: undefined
});
if(!_queried_permissioned) query_devices(); /* we now got permissions, requery devices */ if(!_queried_permissioned) query_devices(); /* we now got permissions, requery devices */
return stream; return stream;
} catch(error) { } catch(error) {

View File

@ -1,19 +1,19 @@
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import * as aplayer from "../audio/player"; import * as aplayer from "../audio/player";
import * as elog from "tc-shared/ui/frames/server_log"; import * as elog from "tc-shared/ui/frames/server_log";
import {BasicCodec} from "../codec/BasicCodec"; import {BasicCodec} from "../codec/BasicCodec";
import {CodecType} from "../codec/Codec"; import {CodecType} from "../codec/Codec";
import {LogCategory} from "tc-shared/log";
import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {CodecWrapperWorker} from "../codec/CodecWrapperWorker"; import {CodecWrapperWorker} from "../codec/CodecWrapperWorker";
import {ServerConnection} from "../connection/ServerConnection"; import {ServerConnection} from "../connection/ServerConnection";
import {voice} from "tc-shared/connection/ConnectionBase"; import {voice} from "tc-shared/connection/ConnectionBase";
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
import {VoiceClientController} from "./VoiceClient"; import {VoiceClientController} from "./VoiceClient";
import {settings} from "tc-shared/settings"; import {settings} from "tc-shared/settings";
import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-shared/voice/RecorderBase"; import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-shared/voice/RecorderBase";
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
import VoiceClient = voice.VoiceClient; import VoiceClient = voice.VoiceClient;
export namespace codec { export namespace codec {
@ -258,31 +258,48 @@ export class VoiceConnection extends AbstractVoiceConnection {
recorder.callback_start = this.handle_local_voice_started.bind(this); recorder.callback_start = this.handle_local_voice_started.bind(this);
recorder.callback_stop = this.handle_local_voice_ended.bind(this); recorder.callback_stop = this.handle_local_voice_ended.bind(this);
if(this._type == VoiceEncodeType.NATIVE_ENCODE) { recorder.callback_input_change = async (old_input, new_input) => {
if(!this.local_audio_stream) if(old_input) {
this.setup_native(); /* requires initialized audio */ try {
await old_input.set_consumer(undefined);
await recorder.input.set_consumer({ } catch(error) {
type: InputConsumerType.NODE, log.warn(LogCategory.VOICE, tr("Failed to release own consumer from old input: %o"), error);
callback_node: node => {
if(!this.local_audio_stream || !this.local_audio_mute)
return;
node.connect(this.local_audio_mute);
},
callback_disconnect: node => {
if(!this.local_audio_mute)
return;
node.disconnect(this.local_audio_mute);
} }
} as NodeInputConsumer); }
} else { if(new_input) {
await recorder.input.set_consumer({ if(this._type == VoiceEncodeType.NATIVE_ENCODE) {
type: InputConsumerType.CALLBACK, if(!this.local_audio_stream)
callback_audio: buffer => this.handle_local_voice(buffer, false) this.setup_native(); /* requires initialized audio */
} as CallbackInputConsumer);
} try {
await new_input.set_consumer({
type: InputConsumerType.NODE,
callback_node: node => {
if(!this.local_audio_stream || !this.local_audio_mute)
return;
node.connect(this.local_audio_mute);
},
callback_disconnect: node => {
if(!this.local_audio_mute)
return;
node.disconnect(this.local_audio_mute);
}
} as NodeInputConsumer);
log.debug(LogCategory.VOICE, tr("Successfully set/updated to the new input for the recorder"));
} catch (e) {
log.warn(LogCategory.VOICE, tr("Failed to set consumer to the new recorder input: %o"), e);
}
} else {
//TODO: Error handling?
await recorder.input.set_consumer({
type: InputConsumerType.CALLBACK,
callback_audio: buffer => this.handle_local_voice(buffer, false)
} as CallbackInputConsumer);
}
}
};
} }
this.connection.client.update_voice_status(undefined); this.connection.client.update_voice_status(undefined);
} }

View File

@ -1,8 +1,6 @@
#!/bin/bash #!/bin/bash
[[ ! -d libraries/opus/out/ ]] && { echo "Missing opus build. Please build it before!"; exit 1; } cd "$(dirname "$0")" || { echo "Failed to enter base dir"; exit 1; }
[[ ! -f libraries/opus/out/lib/libopus.a ]] && { echo "Missing opus static library. Please unsure your opus build was successfull."; exit 1; }
[[ -d build_ ]] && { [[ -d build_ ]] && {
rm -r build_ || { echo "failed to remove old build directory"; exit 1; } rm -r build_ || { echo "failed to remove old build directory"; exit 1; }
} }

@ -0,0 +1 @@
Subproject commit adcb7bc21d0afa79c1975030b29dfeef76651839

View File

@ -10,7 +10,7 @@ Object.assign(config.resolve.alias, {
"tc-shared": path.resolve(__dirname, "shared/js"), "tc-shared": path.resolve(__dirname, "shared/js"),
"tc-backend/web": path.resolve(__dirname, "web/js"), "tc-backend/web": path.resolve(__dirname, "web/js"),
"tc-backend": path.resolve(__dirname, "web/js"), "tc-backend": path.resolve(__dirname, "web/js"),
"tc-generated/codec/opus": path.resolve(__dirname, "asm/generated/TeaWeb-Worker-Codec-Opus.js"), "tc-generated/codec/opus": path.resolve(__dirname, "web/native-codec/generated/TeaWeb-Worker-Codec-Opus.js"),
}); });
export = config; export = config;

View File

@ -98,7 +98,8 @@ export const config = (target: "web" | "client") => { return {
getCustomTransformers: (prog: ts.Program) => { getCustomTransformers: (prog: ts.Program) => {
return { return {
before: [trtransformer(prog, { before: [trtransformer(prog, {
optimized: true optimized: true,
target_file: path.join(__dirname, "dist", "translations.json")
})] })]
}; };
} }
@ -125,8 +126,10 @@ export const config = (target: "web" | "client") => { return {
filename: (chunkData) => { filename: (chunkData) => {
if(chunkData.chunk.name === "loader") if(chunkData.chunk.name === "loader")
return "loader.js"; return "loader.js";
return isDevelopment ? '[name].js' : '[contenthash].js';
return '[name].js';
}, },
chunkFilename: "[name].js",
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
publicPath: "js/" publicPath: "js/"
}, },