Merged with master
commit
cccd780724
|
@ -15,6 +15,7 @@ node_modules/
|
|||
# Some build output
|
||||
/dist/
|
||||
/declarations/
|
||||
/travis-build/
|
||||
|
||||
# Don't add the created packages to git
|
||||
/TeaSpeakUI.tar.gz
|
||||
|
@ -28,4 +29,6 @@ node_modules/
|
|||
/*.js.map
|
||||
|
||||
/webpack/*.js
|
||||
/webpack/*.js.map
|
||||
/webpack/*.js.map
|
||||
|
||||
/files_*.pem
|
|
@ -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"]
|
||||
path = vendor/xbbcode
|
||||
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
|
||||
|
|
20
.travis.yml
20
.travis.yml
|
@ -8,15 +8,19 @@ node_js:
|
|||
services:
|
||||
- docker
|
||||
|
||||
sudo: required
|
||||
|
||||
before_install:
|
||||
# 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
|
||||
- chmod +x ./scripts/travis.sh
|
||||
- chmod +x ./scripts/travis_deploy.sh
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: "build"
|
||||
name: TeaWeb build master branch
|
||||
script:
|
||||
- "./scripts/travis.sh --enable-release --enable-debug || travis_terminate 1;"
|
||||
- "./scripts/travis_deploy.sh || travis_terminate 1;"
|
||||
if: branch = master
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
|
||||
script:
|
||||
- "./scripts/travis.sh --enable-release --enable-debug || travis_terminate 1;"
|
||||
- "./scripts/travis_deploy.sh || travis_terminate 1;"
|
10
ChangeLog.md
10
ChangeLog.md
|
@ -1,4 +1,14 @@
|
|||
# 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**
|
||||
- Fixed a bug within the permission editor which kicks you from the server
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 655cc54c564b84ef2827f0b2152ce3811046201e
|
|
@ -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)
|
|
@ -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();
|
||||
top_menu.update_state(); //TODO: Only run "small" update?
|
||||
}
|
||||
}
|
||||
sync_status_with_server() {
|
||||
if (this.serverConnection.connected())
|
||||
|
|
165
file.ts
165
file.ts
|
@ -9,6 +9,7 @@ import * as mt from "mime-types";
|
|||
import * as os from "os";
|
||||
import {PathLike} from "fs";
|
||||
import {ChildProcess} from "child_process";
|
||||
import * as https from "https";
|
||||
|
||||
/* All project files */
|
||||
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",
|
||||
|
||||
"path": "wasm/",
|
||||
"local-path": "./asm/generated/"
|
||||
"local-path": "./web/native-codec/generated/"
|
||||
},
|
||||
{ /* web css files */
|
||||
"web-only": true,
|
||||
|
@ -342,147 +343,6 @@ const WEB_APP_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
|
||||
declare module "fs-extra" {
|
||||
export function exists(path: PathLike): Promise<boolean>;
|
||||
|
@ -636,6 +496,8 @@ namespace server {
|
|||
let server: http.Server;
|
||||
let php: string;
|
||||
let options: Options;
|
||||
|
||||
const use_https = false;
|
||||
export async function launch(_files: ProjectResource[], options_: Options) {
|
||||
options = options_;
|
||||
files = _files;
|
||||
|
@ -654,7 +516,24 @@ namespace server {
|
|||
console.error("failed to validate php interpreter: %o", error);
|
||||
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) => {
|
||||
server.on('error', reject);
|
||||
server.listen(options.port, () => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
|||
"main": "main.js",
|
||||
"directories": {},
|
||||
"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",
|
||||
"dtsgen": "node tools/dtsgen/index.js",
|
||||
"trgen": "node tools/trgen/index.js",
|
||||
|
@ -17,11 +17,13 @@
|
|||
"build-web": "webpack --config webpack-web.config.js",
|
||||
"watch-web": "webpack --watch --config webpack-web.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)",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@google-cloud/translate": "^5.3.0",
|
||||
"@types/dompurify": "^2.0.1",
|
||||
"@types/emscripten": "^1.38.0",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
|
@ -55,7 +57,7 @@
|
|||
"terser": "^4.2.1",
|
||||
"terser-webpack-plugin": "latest",
|
||||
"ts-loader": "^6.2.2",
|
||||
"typescript": "3.6.5",
|
||||
"typescript": "^3.7.0",
|
||||
"wabt": "^1.0.13",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
|
|
|
@ -42,6 +42,7 @@ if [[ $_exit_code -ne 0 ]]; then
|
|||
fi
|
||||
|
||||
echo "Generating required build tooks"
|
||||
chmod +x ./tools/build_trgen.sh
|
||||
./tools/build_trgen.sh; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build build_typescript translation generator"
|
||||
|
@ -56,14 +57,22 @@ if [[ $_exit_code -ne 0 ]]; then
|
|||
fi
|
||||
|
||||
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
|
||||
echo "Failed to build the XBBCode vendor"
|
||||
exit 1
|
||||
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
|
||||
echo "Packing generated css files"
|
||||
chmod +x ./shared/css/generate_packed.sh
|
||||
./shared/css/generate_packed.sh; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to package generated css files"
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function execute_tsc() {
|
||||
# shellcheck disable=SC2068
|
||||
execute_npm_command tsc $@
|
||||
}
|
||||
|
||||
function execute_ttsc() {
|
||||
execute_npm_command ttsc $@
|
||||
}
|
||||
|
||||
function execute_npm_command() {
|
||||
command_name=$1
|
||||
command_variable="command_$command_name"
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
PACKAGES_DIRECTORY="auto-build/packages/"
|
||||
LOG_FILE="auto-build/logs/build.log"
|
||||
|
||||
if [[ -z "${GIT_AUTHTOKEN}" ]]; then
|
||||
echo "Missing environment variable GIT_AUTHTOKEN. Please set it before usign this script!"
|
||||
exit 1
|
||||
|
@ -23,11 +26,12 @@ if [[ ! -x ${GIT_RELEASE_EXECUTABLE} ]]; then
|
|||
exit 1
|
||||
}
|
||||
|
||||
gunzip /tmp/git-release.gz && chmod +x /tmp/git-release;
|
||||
[[ $? -eq 0 ]] || {
|
||||
gunzip /tmp/git-release.gz; _exit_code=$?;
|
||||
[[ $_exit_code -eq 0 ]] || {
|
||||
echo "Failed to unzip github-release-linux"
|
||||
exit 1
|
||||
}
|
||||
chmod +x /tmp/git-release;
|
||||
|
||||
echo "Download of github-release-linux (1.2.4) finished"
|
||||
else
|
||||
|
@ -40,6 +44,7 @@ if [[ ! -x ${GIT_RELEASE_EXECUTABLE} ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/.." || { echo "Failed to enter base dir"; exit 1; }
|
||||
echo "Generating release"
|
||||
${GIT_RELEASE_EXECUTABLE} release \
|
||||
--repo "TeaWeb" \
|
||||
|
@ -54,7 +59,7 @@ ${GIT_RELEASE_EXECUTABLE} release \
|
|||
}
|
||||
|
||||
echo "Uploading release files"
|
||||
folders=("/tmp/build/logs/" "/tmp/build/packages/")
|
||||
folders=("${LOG_FILE}" "${PACKAGES_DIRECTORY}")
|
||||
uploaded_files=()
|
||||
failed_files=()
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@ fi
|
|||
|
||||
response=$(git diff-index HEAD -- . ':!asm/libraries/' ':!package-lock.json' ':!vendor/')
|
||||
if [[ "$response" != "" ]]; then
|
||||
echo "You're using a private modified build!"
|
||||
echo "Cant assign git hash!"
|
||||
echo "You're using a private modified build! Cant assign git hash!"
|
||||
NAME="TeaWeb-${type}.zip"
|
||||
else
|
||||
NAME="TeaWeb-${type}-$(git rev-parse --short HEAD).zip"
|
||||
|
@ -41,7 +40,8 @@ fi
|
|||
current_path=$(pwd)
|
||||
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!"
|
||||
exit 1
|
||||
fi
|
||||
|
@ -50,4 +50,4 @@ cd "$current_path" || { echo "Failed to reenter source path"; exit 1; }
|
|||
|
||||
mv "${source_path}/${NAME}" .
|
||||
echo "Release package successfully packaged!"
|
||||
echo "Target file: ${NAME}"
|
||||
echo "Target file: ${NAME} ($(pwd))"
|
|
@ -20,4 +20,7 @@ popup/**/*.css.map
|
|||
popup/**/*.js
|
||||
popup/**/*.js.map
|
||||
|
||||
generated/*
|
||||
generated/*
|
||||
|
||||
/*.js
|
||||
/*.js.map
|
|
@ -190,7 +190,7 @@ $border_color_activated: rgba(255, 255, 255, .75);
|
|||
}
|
||||
}
|
||||
|
||||
&:hover.displayed, &.force-show {
|
||||
&:hover.dropdownDisplayed, &.force-show {
|
||||
.dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -930,6 +930,9 @@
|
|||
|
||||
font-size: .85em;
|
||||
color: #3c3c3c;
|
||||
|
||||
width: fit-content;
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
|
@ -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);
|
||||
});
|
|
@ -1,17 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR"
|
||||
cd "$(dirname "$0")" || { echo "Failed to enter base directory"; exit 1; }
|
||||
|
||||
#Generate the script translations
|
||||
npm run ttsc -- -p $(pwd)/tsconfig/tsconfig.json
|
||||
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
|
||||
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=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to generate translations file for the template files"
|
||||
exit 1
|
||||
fi
|
|
@ -12,119 +12,6 @@
|
|||
<!-- navigation bar -->
|
||||
<div class="container-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="buttons">
|
||||
|
@ -148,38 +35,6 @@
|
|||
</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 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
|
@ -96,6 +96,30 @@
|
|||
"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",
|
||||
|
|
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
|
@ -26,11 +26,12 @@ import {Frame} from "tc-shared/ui/frames/chat_frame";
|
|||
import {Hostbanner} from "tc-shared/ui/frames/hostbanner";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
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 {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
|
||||
import * as connection from "tc-backend/connection";
|
||||
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 {
|
||||
HANDLER_DESTROYED,
|
||||
|
@ -344,10 +345,7 @@ export class ConnectionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
if(update_control && server_connections.active_connection_handler() === this) {
|
||||
control_bar.apply_server_state();
|
||||
}
|
||||
|
||||
control_bar_instance()?.events().fire("server_updated", { category: "settings-initialized", handler: this });
|
||||
}
|
||||
|
||||
get connected() : boolean {
|
||||
|
@ -382,7 +380,8 @@ export class ConnectionHandler {
|
|||
if(pathname.endsWith(".php"))
|
||||
pathname = pathname.substring(0, pathname.lastIndexOf("/"));
|
||||
|
||||
if(bipc.supported()) {
|
||||
/* certaccept is currently not working! */
|
||||
if(bipc.supported() && false) {
|
||||
tag.attr('href', "#");
|
||||
let popup: Window;
|
||||
tag.on('click', event => {
|
||||
|
@ -607,8 +606,7 @@ export class ConnectionHandler {
|
|||
if(this.serverConnection)
|
||||
this.serverConnection.disconnect();
|
||||
|
||||
if(control_bar.current_connection_handler() == this)
|
||||
control_bar.update_connection_state();
|
||||
this.on_connection_state_changed(); /* really required to call? */
|
||||
this.side_bar.private_conversations().clear_client_ids();
|
||||
this.hostbanner.update();
|
||||
|
||||
|
@ -642,8 +640,7 @@ export class ConnectionHandler {
|
|||
}
|
||||
|
||||
private on_connection_state_changed() {
|
||||
if(control_bar.current_connection_handler() == this)
|
||||
control_bar.update_connection_state();
|
||||
control_bar_instance()?.events().fire("server_updated", { category: "connection-state", handler: this });
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -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)
|
||||
return;
|
||||
|
||||
|
@ -789,7 +787,8 @@ export class ConnectionHandler {
|
|||
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() {
|
||||
|
|
|
@ -216,6 +216,7 @@ export namespace bbcode {
|
|||
}
|
||||
});
|
||||
|
||||
const load_callback = guid();
|
||||
/* the image parse & displayer */
|
||||
xbbcode.register.register_parser({
|
||||
tag: ["img", "image"],
|
||||
|
@ -238,10 +239,11 @@ export namespace bbcode {
|
|||
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);
|
||||
}
|
||||
})
|
||||
});
|
||||
window[load_callback] = load_image;
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
|
|
@ -52,7 +52,7 @@ export enum BookmarkType {
|
|||
}
|
||||
|
||||
export interface Bookmark {
|
||||
type: /* BookmarkType.ENTRY */ BookmarkType;
|
||||
type: BookmarkType.ENTRY;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
|
||||
server_properties: ServerProperties;
|
||||
|
@ -70,7 +70,7 @@ export interface Bookmark {
|
|||
}
|
||||
|
||||
export interface DirectoryBookmark {
|
||||
type: /* BookmarkType.DIRECTORY */ BookmarkType;
|
||||
type: BookmarkType.DIRECTORY;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
|
||||
readonly content: (Bookmark | DirectoryBookmark)[];
|
||||
|
|
|
@ -58,6 +58,8 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
|
||||
|
||||
register_single_handler(handler: SingleCommandHandler) {
|
||||
if(typeof handler.command === "string")
|
||||
handler.command = [handler.command];
|
||||
this.single_command_handler.push(handler);
|
||||
}
|
||||
|
||||
|
@ -82,7 +84,8 @@ export abstract class AbstractCommandHandlerBoss {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
|
|
|
@ -263,38 +263,53 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
}
|
||||
|
||||
request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> {
|
||||
let bulked_response = false;
|
||||
let bulk_index = 0;
|
||||
|
||||
const result: PlaylistSong[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const single_handler: SingleCommandHandler = {
|
||||
command: "notifyplaylistsonglist",
|
||||
command: ["notifyplaylistsonglist", "notifyplaylistsonglistfinished"],
|
||||
function: command => {
|
||||
const json = command.arguments;
|
||||
|
||||
if(json[0]["playlist_id"] != playlist_id) {
|
||||
log.error(LogCategory.NETWORKING, tr("Received invalid notification for playlist songs"));
|
||||
return false;
|
||||
if(bulk_index === 0) {
|
||||
/* we're sending the response as bulk */
|
||||
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) {
|
||||
try {
|
||||
result.push({
|
||||
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"],
|
||||
if(command.command === "notifyplaylistsonglistfinished") {
|
||||
resolve(result);
|
||||
return true;
|
||||
} else {
|
||||
for(const entry of json) {
|
||||
try {
|
||||
result.push({
|
||||
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_metadata: entry["song_metadata"]
|
||||
});
|
||||
} catch(error) {
|
||||
log.error(LogCategory.NETWORKING, tr("Failed to parse playlist song entry: %o"), error);
|
||||
song_loaded: entry["song_loaded"] == true || entry["song_loaded"] == "1",
|
||||
song_metadata: entry["song_metadata"]
|
||||
});
|
||||
} catch(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);
|
||||
|
|
|
@ -121,7 +121,7 @@ export class ServerCommand {
|
|||
|
||||
export interface SingleCommandHandler {
|
||||
name?: string;
|
||||
command?: string;
|
||||
command?: string | string[];
|
||||
timeout?: number;
|
||||
|
||||
/* if the return is true then the command handler will be removed */
|
||||
|
|
|
@ -1,29 +1,37 @@
|
|||
//TODO: Combine EventConvert and Event?
|
||||
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import * as React from "react";
|
||||
|
||||
export interface EventConvert<All> {
|
||||
as<T extends keyof All>() : All[T];
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
export interface Event<Events, T = keyof Events> {
|
||||
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();
|
||||
|
||||
readonly type = "singletone-instance";
|
||||
private constructor() { }
|
||||
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
|
||||
}
|
||||
|
||||
const event_annotation_key = guid();
|
||||
export class Registry<Events> {
|
||||
private readonly registry_uuid;
|
||||
|
||||
private handler: {[key: string]: ((event) => void)[]} = {};
|
||||
private connections: {[key: string]:Registry<string>[]} = {};
|
||||
private event_handler_objects: {
|
||||
object: any,
|
||||
handlers: {[key: string]: ((event) => void)[]}
|
||||
}[] = [];
|
||||
private debug_prefix = undefined;
|
||||
private warn_unhandled_events = true;
|
||||
|
||||
constructor() {
|
||||
this.registry_uuid = "evreg_data_" + guid();
|
||||
|
@ -33,8 +41,11 @@ export class Registry<Events> {
|
|||
enable_debug(prefix: string) { this.debug_prefix = prefix || "---"; }
|
||||
disable_debug() { this.debug_prefix = undefined; }
|
||||
|
||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<T> & EventConvert<Events>) => void);
|
||||
on(events: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void);
|
||||
enable_warn_unhandled_events() { this.warn_unhandled_events = true; }
|
||||
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) {
|
||||
if(!Array.isArray(events))
|
||||
events = [events];
|
||||
|
@ -49,8 +60,8 @@ export class Registry<Events> {
|
|||
}
|
||||
|
||||
/* one */
|
||||
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<T> & EventConvert<Events>) => void);
|
||||
one(events: (keyof Events)[], handler: (event?: Event<keyof Events> & 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<Events, keyof Events>) => void);
|
||||
one(events, handler) {
|
||||
if(!Array.isArray(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>(event: T, handler: (event?: Event<T> & EventConvert<Events>) => void);
|
||||
off(event: (keyof Events)[], handler: (event?: Event<keyof Events> & EventConvert<Events>) => void);
|
||||
off<T extends keyof Events>(handler: (event?: Event<Events, T>) => void);
|
||||
off<T extends keyof Events>(event: T, handler: (event?: Event<Events, T>) => void);
|
||||
off(event: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
|
||||
off(handler_or_events, handler?) {
|
||||
if(typeof handler_or_events === "function") {
|
||||
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>) {
|
||||
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
||||
connect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
||||
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>) {
|
||||
(this.connections[event as string] || []).remove(target as any);
|
||||
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: Registry<EOther>) {
|
||||
for(const event of Array.isArray(events) ? events : [events])
|
||||
(this.connections[event as string] || []).remove(target as any);
|
||||
}
|
||||
|
||||
disconnect_all<EOther>(target: Registry<EOther>) {
|
||||
|
@ -102,24 +115,113 @@ export class Registry<Events> {
|
|||
as: function () { return this; }
|
||||
});
|
||||
|
||||
let invoke_count = 0;
|
||||
for(const handler of (this.handler[event_type as string] || [])) {
|
||||
handler(event);
|
||||
invoke_count++;
|
||||
|
||||
const reg_data = handler[this.registry_uuid];
|
||||
if(typeof reg_data === "object" && reg_data.singleshot)
|
||||
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);
|
||||
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]) {
|
||||
setTimeout(() => this.fire(event_type, data));
|
||||
}
|
||||
|
||||
destory() {
|
||||
destroy() {
|
||||
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
|
||||
const eclient = new events.Registry<events.channel_tree.client>();
|
||||
const emusic = new events.Registry<events.sidebar.music>();
|
||||
const eclient = new Registry<channel_tree.client>();
|
||||
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);
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@ import * as fidentity from "./profiles/identities/TeaForumIdentity";
|
|||
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||
import * as cmanager 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 * as top_menu from "./ui/frames/MenuBar";
|
||||
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 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 */
|
||||
require("./proto").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 */
|
||||
|
||||
const js_render = window.jsrender || $;
|
||||
|
@ -140,6 +146,8 @@ async function initialize() {
|
|||
bipc.setup();
|
||||
}
|
||||
|
||||
export let client_control_events: Registry<ClientGlobalControlEvents>;
|
||||
|
||||
async function initialize_app() {
|
||||
try { //Initialize main template
|
||||
const main = $("#tmpl_main").renderTag({
|
||||
|
@ -154,7 +162,15 @@ async function initialize_app() {
|
|||
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())
|
||||
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]);
|
||||
|
||||
|
||||
(<any>window).test_upload = (message?: string) => {
|
||||
(window as any).test_upload = (message?: string) => {
|
||||
message = message || "Hello World";
|
||||
|
||||
const connection = server_connections.active_connection_handler();
|
|
@ -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"]);
|
||||
for(let key in Object.keys(groupData)) {
|
||||
for(let key of Object.keys(groupData)) {
|
||||
if(key == "sgid") continue;
|
||||
if(key == "cgid") continue;
|
||||
if(key == "type") continue;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as log 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 {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ChannelTree} from "tc-shared/ui/view";
|
|||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import * as log 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 * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {Sound} from "tc-shared/sound/Sounds";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -73,7 +73,7 @@ export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
|
|||
const parent_offset = slider.offset();
|
||||
const min = parent_offset.left;
|
||||
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 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);
|
||||
};
|
||||
|
||||
slider.on('mousedown', event => {
|
||||
slider.on('mousedown touchstart', ((event: MouseEvent | TouchEvent) => {
|
||||
document.addEventListener('mousemove', mouse_listener);
|
||||
document.addEventListener('touchmove', mouse_listener);
|
||||
|
||||
|
@ -93,7 +93,9 @@ export function sliderfy(slider: JQuery, options?: SliderOptions) : Slider {
|
|||
|
||||
tool.show();
|
||||
slider.addClass("active");
|
||||
});
|
||||
|
||||
mouse_listener(event);
|
||||
}) as any);
|
||||
|
||||
update_value(options.initial_value, false);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,17 +14,17 @@ import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
|
|||
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
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 {spawnQueryManage} from "tc-shared/ui/modal/ModalQueryManage";
|
||||
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {spawnAbout} from "tc-shared/ui/modal/ModalAbout";
|
||||
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 {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import * as slog from "tc-shared/ui/frames/server_log";
|
||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||
|
||||
export interface HRItem { }
|
||||
|
||||
|
@ -348,7 +348,8 @@ export function initialize() {
|
|||
handler.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
handler.log.log(slog.Type.DISCONNECTED, {});
|
||||
}
|
||||
control_bar.update_connection_state();
|
||||
|
||||
control_bar_instance()?.events().fire("update_state", { state: "connect-state" });
|
||||
update_state();
|
||||
};
|
||||
item = menu.append_item(tr("Disconnect from current server"));
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {control_bar} from "tc-shared/ui/frames/ControlBar";
|
||||
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 function initialize(manager: ServerConnectionManager) {
|
||||
|
@ -97,7 +98,7 @@ export class ServerConnectionManager {
|
|||
handler.resize_elements();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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!
|
|
@ -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 */
|
|
@ -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"}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 */
|
|
@ -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"}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -4,6 +4,7 @@ import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
|||
import * as htmltags from "tc-shared/ui/htmltags";
|
||||
import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {formatDate} from "tc-shared/MessageFormatter";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
|
||||
export enum Type {
|
||||
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 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)]
|
||||
}
|
||||
};
|
||||
function register_message_builder<T extends keyof TypeInfo>(key: T, builder: MessageBuilder<T>) {
|
||||
MessageBuilders[key] = builder;
|
||||
}
|
||||
|
||||
export class ServerLog {
|
||||
private readonly handle: ConnectionHandler;
|
||||
|
@ -384,27 +388,27 @@ const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.genera
|
|||
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));
|
||||
};
|
||||
|
||||
MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve, options) => 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_resolve_error"] = (data: event.ConnectionHostnameResolveError, options) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message);
|
||||
MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve) => formatMessage(tr("Resolving hostname"));
|
||||
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) => 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_failed"] = (data: event.ConnectionFailed, options) => formatMessage(tr("Connect failed."));
|
||||
MessageBuilders["connection_connected"] = (data: event.ConnectionConnected, options) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true));
|
||||
MessageBuilders["connection_login"] = () => formatMessage(tr("Logging in..."));
|
||||
MessageBuilders["connection_failed"] = () => formatMessage(tr("Connect failed."));
|
||||
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"));
|
||||
};
|
||||
|
||||
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"));
|
||||
};
|
||||
|
||||
MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => {
|
||||
MessageBuilders["client_view_enter"] = (data: event.ClientEnter) => {
|
||||
if(data.reason == ViewReasonId.VREASON_SYSTEM) {
|
||||
return undefined;
|
||||
} 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 + ")")];
|
||||
};
|
||||
|
||||
MessageBuilders["client_view_move"] = (data: event.ClientMove, options) => {
|
||||
MessageBuilders["client_view_move"] = (data: event.ClientMove) => {
|
||||
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}"),
|
||||
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 + ")")];
|
||||
};
|
||||
|
||||
MessageBuilders["client_view_leave"] = (data: event.ClientLeave, options) => {
|
||||
MessageBuilders["client_view_leave"] = (data: event.ClientLeave) => {
|
||||
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));
|
||||
} 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 + ")")];
|
||||
};
|
||||
|
||||
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => {
|
||||
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => {
|
||||
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]");
|
||||
};
|
||||
|
||||
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => {
|
||||
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => {
|
||||
if(data.own_client) {
|
||||
return formatMessage(tr("Nickname successfully changed."));
|
||||
return tra("Nickname successfully changed.");
|
||||
} 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 */
|
||||
};
|
||||
|
||||
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")))
|
||||
};
|
||||
|
||||
MessageBuilders["reconnect_canceled"] = (data: event.ReconnectCanceled, options) => {
|
||||
MessageBuilders["reconnect_canceled"] = () => {
|
||||
return tra("Canceled reconnect.")
|
||||
};
|
||||
|
||||
MessageBuilders["reconnect_execute"] = (data: event.ReconnectExecute, options) => {
|
||||
MessageBuilders["reconnect_execute"] = () => {
|
||||
return tra("Reconnecting...")
|
||||
};
|
||||
|
||||
MessageBuilders["server_banned"] = (data: event.ServerBanned, options) => {
|
||||
MessageBuilders["server_banned"] = (data: event.ServerBanned) => {
|
||||
let result: JQuery[];
|
||||
|
||||
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"));
|
||||
};
|
||||
};
|
||||
|
||||
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));
|
||||
});
|
|
@ -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 {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
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 {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
|
|
|
@ -66,7 +66,7 @@ export class MusicInfo {
|
|||
|
||||
destroy() {
|
||||
this.set_current_bot(undefined);
|
||||
this.events.destory();
|
||||
this.events.destroy();
|
||||
|
||||
this._html_tag && this._html_tag.remove();
|
||||
this._html_tag = undefined;
|
||||
|
|
|
@ -4,6 +4,7 @@ import {ChannelEntry} from "tc-shared/ui/channel";
|
|||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import {htmlEscape} from "tc-shared/ui/frames/chat";
|
||||
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};
|
||||
|
||||
|
@ -30,6 +31,7 @@ export interface ChannelProperties {
|
|||
add_braces?: boolean
|
||||
}
|
||||
|
||||
const callback_object_id = guid();
|
||||
/* required for the bbcodes */
|
||||
function generate_client_open(properties: ClientProperties) : string {
|
||||
let result = "";
|
||||
|
@ -57,7 +59,7 @@ function generate_client_open(properties: ClientProperties) : string {
|
|||
}
|
||||
|
||||
/* 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 + ">";
|
||||
return result;
|
||||
|
@ -100,7 +102,7 @@ function generate_channel_open(properties: ChannelProperties) : string {
|
|||
result = result + "channel-name='" + encodeURIComponent(properties.channel_name) + "' ";
|
||||
|
||||
/* 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 + ">";
|
||||
return result;
|
||||
|
@ -186,6 +188,7 @@ export namespace callbacks {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
window[callback_object_id] = callbacks;
|
||||
|
||||
declare const xbbcode;
|
||||
namespace bbcodes {
|
||||
|
|
|
@ -16,8 +16,8 @@ import {LogCategory} from "tc-shared/log";
|
|||
import * as log from "tc-shared/log";
|
||||
import * as i18nc from "tc-shared/i18n/country";
|
||||
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 {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||
|
||||
export function spawnBookmarkModal() {
|
||||
let modal: Modal;
|
||||
|
@ -375,7 +375,7 @@ export function spawnBookmarkModal() {
|
|||
|
||||
modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks");
|
||||
modal.close_listener.push(() => {
|
||||
control_bar.update_bookmarks();
|
||||
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
|
||||
top_menu.rebuild_bookmarks();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import * as loader from "tc-loader";
|
||||
import { modal as emodal } from "tc-shared/events";
|
||||
import {modal_settings} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {profiles} from "tc-shared/profiles/ConnectionProfile";
|
||||
|
|
|
@ -198,7 +198,7 @@ function settings_general_language(container: JQuery, modal: Modal) {
|
|||
};
|
||||
|
||||
const update_current_selected = () => {
|
||||
const container_current = container.find(".selected-language6");
|
||||
const container_current = container.find(".selected-language");
|
||||
container_current.empty().text(tr("Loading"));
|
||||
|
||||
let current_translation: RepositoryTranslation;
|
||||
|
@ -1782,7 +1782,7 @@ export namespace modal_settings {
|
|||
console.debug(tr("Changed default microphone device"));
|
||||
event_registry.fire_async("set-device-result", { status: "success", device_id: event.device_id });
|
||||
}).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 });
|
||||
});
|
||||
});
|
||||
|
@ -1959,7 +1959,7 @@ export namespace modal_settings {
|
|||
const tags = volume_bar_tags[device.device_id];
|
||||
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;
|
||||
else if(level < 0) level = 0;
|
||||
tags.error.attr('title', device.error || null).text(device.error || null);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
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 {createErrorModal, createInfoModal, createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import {HTMLPermissionEditor} from "tc-shared/ui/modal/permission/HTMLPermissionEditor";
|
||||
|
|
|
@ -11,9 +11,9 @@ import {createServerModal} from "tc-shared/ui/modal/ModalServerEdit";
|
|||
import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect";
|
||||
import {spawnAvatarList} from "tc-shared/ui/modal/ModalAvatarList";
|
||||
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 * as top_menu from "./frames/MenuBar";
|
||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||
|
||||
export class ServerProperties {
|
||||
virtualserver_host: string = "";
|
||||
|
@ -318,7 +318,8 @@ export class ServerEntry {
|
|||
});
|
||||
bookmarks.save_bookmark();
|
||||
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)
|
||||
|
@ -332,8 +333,7 @@ export class ServerEntry {
|
|||
if(update_bannner)
|
||||
this.channelTree.client.hostbanner.update();
|
||||
if(update_button)
|
||||
if(control_bar.current_connection_handler() === this.channelTree.client)
|
||||
control_bar.apply_server_hostbutton();
|
||||
control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" });
|
||||
|
||||
group.end();
|
||||
if(is_self_notify && this.info_request_promise_resolve) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import * as log from "tc-shared/log";
|
||||
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 {KeyCode, SpecialKey} from "tc-shared/PPTListener";
|
||||
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
|
|
|
@ -46,7 +46,7 @@ export class RecorderProfile {
|
|||
|
||||
current_handler: ConnectionHandler;
|
||||
|
||||
callback_support_change: () => any;
|
||||
callback_input_change: (old_input: AbstractInput, new_input: AbstractInput) => Promise<void>;
|
||||
callback_start: () => any;
|
||||
callback_stop: () => any;
|
||||
|
||||
|
@ -107,6 +107,9 @@ export class RecorderProfile {
|
|||
if(this.callback_stop)
|
||||
this.callback_stop();
|
||||
};
|
||||
|
||||
//TODO: Await etc?
|
||||
this.callback_input_change && this.callback_input_change(undefined, this.input);
|
||||
}
|
||||
|
||||
private async load() {
|
||||
|
@ -199,6 +202,7 @@ export class RecorderProfile {
|
|||
}
|
||||
}
|
||||
|
||||
this.callback_input_change = undefined;
|
||||
this.callback_start = undefined;
|
||||
this.callback_stop = undefined;
|
||||
this.callback_unmount = undefined;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"baseUrl": ".",
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"lib": ["es6", "dom"],
|
||||
"lib": ["es6", "dom"]
|
||||
|
||||
/*
|
||||
"typeRoots": [],
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
"compiler.ts",
|
||||
"jsrender_generator.ts",
|
||||
"ts_generator.ts",
|
||||
"ttsc_transformer.ts"
|
||||
//"ttsc_transformer.ts"
|
||||
]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/* General file with least possible errors. This is just for your IDE (PHP-Storm for example) */
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
|
|
|
@ -458,11 +458,14 @@ class JavascriptInput implements AbstractInput {
|
|||
audio_constrains.groupId = group_id;
|
||||
|
||||
audio_constrains.echoCancellation = true;
|
||||
/* may supported */ (audio_constrains as any).autoGainControl = true;
|
||||
/* may supported */ (audio_constrains as any).noiseSuppression = true;
|
||||
audio_constrains.autoGainControl = true;
|
||||
audio_constrains.noiseSuppression = true;
|
||||
/* 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 */
|
||||
return stream;
|
||||
} catch(error) {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as loader from "tc-loader";
|
||||
import * as aplayer from "../audio/player";
|
||||
import * as elog from "tc-shared/ui/frames/server_log";
|
||||
import {BasicCodec} from "../codec/BasicCodec";
|
||||
import {CodecType} from "../codec/Codec";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {CodecWrapperWorker} from "../codec/CodecWrapperWorker";
|
||||
import {ServerConnection} from "../connection/ServerConnection";
|
||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||
import {VoiceClientController} from "./VoiceClient";
|
||||
import {settings} from "tc-shared/settings";
|
||||
import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-shared/voice/RecorderBase";
|
||||
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
|
||||
import VoiceClient = voice.VoiceClient;
|
||||
|
||||
export namespace codec {
|
||||
|
@ -258,31 +258,48 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
recorder.callback_start = this.handle_local_voice_started.bind(this);
|
||||
recorder.callback_stop = this.handle_local_voice_ended.bind(this);
|
||||
|
||||
if(this._type == VoiceEncodeType.NATIVE_ENCODE) {
|
||||
if(!this.local_audio_stream)
|
||||
this.setup_native(); /* requires initialized audio */
|
||||
|
||||
await recorder.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);
|
||||
recorder.callback_input_change = async (old_input, new_input) => {
|
||||
if(old_input) {
|
||||
try {
|
||||
await old_input.set_consumer(undefined);
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.VOICE, tr("Failed to release own consumer from old input: %o"), error);
|
||||
}
|
||||
} as NodeInputConsumer);
|
||||
} else {
|
||||
await recorder.input.set_consumer({
|
||||
type: InputConsumerType.CALLBACK,
|
||||
callback_audio: buffer => this.handle_local_voice(buffer, false)
|
||||
} as CallbackInputConsumer);
|
||||
}
|
||||
}
|
||||
if(new_input) {
|
||||
if(this._type == VoiceEncodeType.NATIVE_ENCODE) {
|
||||
if(!this.local_audio_stream)
|
||||
this.setup_native(); /* requires initialized audio */
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
[[ ! -d libraries/opus/out/ ]] && { echo "Missing opus build. Please build it before!"; exit 1; }
|
||||
[[ ! -f libraries/opus/out/lib/libopus.a ]] && { echo "Missing opus static library. Please unsure your opus build was successfull."; exit 1; }
|
||||
|
||||
cd "$(dirname "$0")" || { echo "Failed to enter base dir"; exit 1; }
|
||||
[[ -d build_ ]] && {
|
||||
rm -r build_ || { echo "failed to remove old build directory"; exit 1; }
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit adcb7bc21d0afa79c1975030b29dfeef76651839
|
|
@ -10,7 +10,7 @@ Object.assign(config.resolve.alias, {
|
|||
"tc-shared": path.resolve(__dirname, "shared/js"),
|
||||
"tc-backend/web": 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;
|
|
@ -98,7 +98,8 @@ export const config = (target: "web" | "client") => { return {
|
|||
getCustomTransformers: (prog: ts.Program) => {
|
||||
return {
|
||||
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) => {
|
||||
if(chunkData.chunk.name === "loader")
|
||||
return "loader.js";
|
||||
return isDevelopment ? '[name].js' : '[contenthash].js';
|
||||
|
||||
return '[name].js';
|
||||
},
|
||||
chunkFilename: "[name].js",
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: "js/"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue