Merged with the master branch
|
@ -1,23 +1,31 @@
|
|||
generated/
|
||||
node_modules/
|
||||
auth/certs/
|
||||
auth/js/auth.js.map
|
||||
package-lock.json
|
||||
|
||||
.sass-cache/
|
||||
# Global ignorings
|
||||
.idea/
|
||||
node_modules/
|
||||
.sass-cache/
|
||||
|
||||
vendor/emoji-picker/**/*.js
|
||||
vendor/emoji-picker/**/*.js.map
|
||||
/auth/certs/
|
||||
/auth/js/auth.js.map
|
||||
|
||||
.package-lock.json
|
||||
/.idea/
|
||||
|
||||
TeaSpeakUI.tar.gz
|
||||
TeaWeb-*.zip
|
||||
/vendor/emoji-picker/**/*.js
|
||||
/vendor/emoji-picker/**/*.js.map
|
||||
!/vendor/emoji-picker/webpack.config.js
|
||||
|
||||
todo.txt
|
||||
# Some build output
|
||||
/dist/
|
||||
/declarations/
|
||||
|
||||
tmp/
|
||||
# Don't add the created packages to git
|
||||
/TeaSpeakUI.tar.gz
|
||||
/TeaWeb-*.zip
|
||||
|
||||
file.js
|
||||
file.js.map
|
||||
/todo.txt
|
||||
/tmp/
|
||||
|
||||
# All out config files are .ts files
|
||||
/*.js
|
||||
/*.js.map
|
||||
|
||||
/webpack/*.js
|
||||
/webpack/*.js.map
|
11
.travis.yml
|
@ -1,19 +1,22 @@
|
|||
sudo: true
|
||||
dist: trusty
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "12"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
- docker pull $DOCKER_USERNAME/teaweb:build_new
|
||||
# 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
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: "build"
|
||||
name: TeaWeb build master branch
|
||||
script:
|
||||
- "mkdir -p /tmp/build"
|
||||
- "docker run --rm -v /tmp/build/logs/:/build/logs/ -v /tmp/build/packages/:/build/packages/ -v `pwd`:/build/TeaWeb $DOCKER_USERNAME/teaweb:build_new --enable-release --enable-debug || travis_terminate 1;"
|
||||
- "./scripts/travis.sh --enable-release --enable-debug || travis_terminate 1;"
|
||||
- "./scripts/travis_deploy.sh || travis_terminate 1;"
|
||||
if: branch = master
|
40
ChangeLog.md
|
@ -1,4 +1,43 @@
|
|||
# Changelog:
|
||||
* **28.03.20**
|
||||
- Fixed a bug within the permission editor which kicks you from the server
|
||||
|
||||
* **21.03.20**
|
||||
- Fixed identity import throwing an "btoa" error
|
||||
|
||||
* **19.03.20**
|
||||
- Using proper icons for the client info
|
||||
- Added an image preview overlay
|
||||
- Added image preview within the chat
|
||||
- Added an image preview to the music bot thumbnails
|
||||
- Added an image preview to client avatars
|
||||
- Fixed the translations system default repository
|
||||
|
||||
* **18.03.20**
|
||||
- Updated the sound playback mechanism and allowing the native backend to playback sounds via the native interface.
|
||||
|
||||
* **22.02.20**
|
||||
- Added a music bot control panel
|
||||
|
||||
* **16.02.20**
|
||||
- Updated the `setup_windows.md` tutorial
|
||||
- Correct redirecting to `index.php` when using the serve mode
|
||||
- Added correct hostname resolving for the web client.
|
||||
- Fixed automatically added new lines for inserted text
|
||||
- Improved button icon visibility within the permission editor
|
||||
- Fixed missing "private chats" button
|
||||
- Allowing YT videos within the chat and channel descriptions
|
||||
|
||||
* **02.02.20**
|
||||
- Added a music bot GUI
|
||||
|
||||
* **30.01.20**
|
||||
- Improved chat message parsing
|
||||
- Fixed copy & paste error
|
||||
- Better handling for spaces & tab indention
|
||||
- Fixed internal audio codec error
|
||||
- Fixed modal spam on microphone start fail
|
||||
|
||||
* **21.12.19**
|
||||
- Improved background performance when the microphone has been muted
|
||||
- Added support for `[ul]` and `[ol]` tags within the chat
|
||||
|
@ -19,6 +58,7 @@
|
|||
- Improved "About" modal overflow behaviour
|
||||
- Allow the client to use the scroll bar without closing the modal within modals
|
||||
- Improved bookmarks modal for smaller devices
|
||||
- Fixed invalid white space representation
|
||||
|
||||
* **10.12.19**
|
||||
- Show the server online count along the server chat
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
generated/
|
||||
build/
|
||||
build_/
|
||||
libraries/opus/build_
|
||||
libraries/opus/out
|
||||
cmake-build-*/
|
||||
libraries/opus/*
|
|
@ -1,21 +1,25 @@
|
|||
cmake_minimum_required(VERSION 3.9)
|
||||
project(TeaWeb-Native)
|
||||
|
||||
set (CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "") #Override some config values from the parent project
|
||||
set(CMAKE_CXX_COMPILER "emcc")
|
||||
set(CMAKE_C_COMPILER "emcc")
|
||||
set(CMAKE_C_LINK_EXECUTABLE "emcc")
|
||||
set (CMAKE_CXX_STANDARD 17)
|
||||
|
||||
|
||||
function(import_opus)
|
||||
# Native SIMD isn't supported yet by most browsers (only experimental)
|
||||
# But since opus already detects if emscripten is able to handle SIMD we have no need to disable this explicitly
|
||||
|
||||
# Disable the math.h warning spam:
|
||||
# #warning "Don't have the functions lrint() and lrintf ()."
|
||||
# #warning "Replacing these functions with a standard C cast."
|
||||
set(CMAKE_C_FLAGS "-Wno-#warnings")
|
||||
set(OPUS_STACK_PROTECTOR OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(libraries/opus/)
|
||||
endfunction()
|
||||
import_opus()
|
||||
|
||||
set(CMAKE_CXX_FLAGS "-O3 --llvm-lto 1 --memory-init-file 0 -s WASM=1 -s ASSERTIONS=1") # -s ALLOW_MEMORY_GROWTH=1 -O3
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "-s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\", \"Pointer_stringify\"]'") #
|
||||
#add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "-s EXPORTED_FUNCTIONS='[\"_malloc\", \"_free\"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]' -s ENVIRONMENT='worker' --pre-js ${CMAKE_SOURCE_DIR}/init.js") #
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/generated/")
|
||||
|
||||
include_directories(libraries/tommath/)
|
||||
include_directories(libraries/tomcrypt/src/headers)
|
||||
include_directories(libraries/opus/include/)
|
||||
add_definitions(-DLTM_DESC)
|
||||
|
||||
add_executable(TeaWeb-Worker-Codec-Opus src/opus.cpp)
|
||||
target_link_libraries(TeaWeb-Worker-Codec-Opus ${CMAKE_CURRENT_SOURCE_DIR}/libraries/opus/.libs/libopus.a)
|
||||
target_link_libraries(TeaWeb-Worker-Codec-Opus opus)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#!/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; }
|
||||
|
||||
[[ -d build_ ]] && {
|
||||
rm -r build_ || { echo "failed to remove old build directory"; exit 1; }
|
||||
}
|
||||
mkdir build_ || exit 1
|
||||
cd build_ || exit 1
|
||||
|
||||
emcmake cmake .. || {
|
||||
echo "Failed to execute cmake"
|
||||
exit 1
|
||||
}
|
||||
|
||||
emmake make -j"$(nproc --all)" || {
|
||||
echo "Failed to build file"
|
||||
exit 1
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
if [[ -d generated/ ]]; then
|
||||
rm -r generated
|
||||
[[ $? -ne 0 ]] && {
|
||||
|
@ -13,19 +15,19 @@ mkdir generated
|
|||
exit 1
|
||||
}
|
||||
|
||||
$(curl --version &> /dev/null)
|
||||
[[ $? -ne 0 ]] && {
|
||||
curl --version &> /dev/null; _exit_code=$?
|
||||
[[ $_exit_code -ne 0 ]] && {
|
||||
echo "Missing CURL. Please install it"
|
||||
exit 1
|
||||
}
|
||||
|
||||
curl https://web.teaspeak.de/wasm/TeaWeb-Worker-Codec-Opus.js --output generated/TeaWeb-Worker-Codec-Opus.js
|
||||
[[ $? -ne 0 ]] && {
|
||||
curl https://web.teaspeak.de/wasm/TeaWeb-Worker-Codec-Opus.js --output generated/TeaWeb-Worker-Codec-Opus.js; _exit_code=$?
|
||||
[[ $_exit_code -ne 0 ]] && {
|
||||
echo "Failed to download opus worker library"
|
||||
exit 1
|
||||
}
|
||||
curl https://web.teaspeak.de/wasm/TeaWeb-Worker-Codec-Opus.wasm --output generated/TeaWeb-Worker-Codec-Opus.wasm
|
||||
[[ $? -ne 0 ]] && {
|
||||
curl https://web.teaspeak.de/wasm/TeaWeb-Worker-Codec-Opus.wasm --output generated/TeaWeb-Worker-Codec-Opus.wasm; _exit_code=$?
|
||||
[[ $_exit_code -ne 0 ]] && {
|
||||
echo "Failed to download opus worker library natives"
|
||||
exit 1
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
for(const callback of Array.isArray(self.__init_em_module) ? self.__init_em_module : [])
|
||||
callback(Module);
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
base_dir=`pwd`
|
||||
cd "$(dirname $0)/libraries/opus/"
|
||||
|
||||
git checkout v1.1.2
|
||||
./autogen.sh
|
||||
emconfigure ./configure --disable-extra-programs --disable-doc --disable-rtcd
|
||||
emmake make
|
||||
|
||||
cd ${base_dir}
|
219
asm/src/opus.cpp
|
@ -1,133 +1,106 @@
|
|||
#include <opus.h>
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <emscripten.h>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
typedef std::unique_ptr<OpusEncoder, decltype(opus_encoder_destroy)*> opus_encoder_t;
|
||||
typedef std::unique_ptr<OpusDecoder, decltype(opus_decoder_destroy)*> opus_decoder_t;
|
||||
struct OpusHandle {
|
||||
opus_encoder_t encoder{nullptr, opus_encoder_destroy};
|
||||
opus_decoder_t decoder{nullptr, opus_decoder_destroy};
|
||||
|
||||
size_t channelCount{1};
|
||||
size_t sampleRate{48000};
|
||||
int opusType{OPUS_APPLICATION_AUDIO};
|
||||
};
|
||||
|
||||
constexpr std::array<std::string_view, 7> opus_errors = {
|
||||
"One or more invalid/out of range arguments", //-1 (OPUS_BAD_ARG)
|
||||
"Not enough bytes allocated in the buffer", //-2 (OPUS_BUFFER_TOO_SMALL)
|
||||
"An internal error was detected", //-3 (OPUS_INTERNAL_ERROR)
|
||||
"The compressed data passed is corrupted", //-4 (OPUS_INVALID_PACKET)
|
||||
"Invalid/unsupported request number", //-5 (OPUS_UNIMPLEMENTED)
|
||||
"An encoder or decoder structure is invalid or already freed", //-6 (OPUS_INVALID_STATE)
|
||||
"Memory allocation has failed" //-7 (OPUS_ALLOC_FAIL)
|
||||
};
|
||||
|
||||
inline std::string_view opus_error_message(int error) {
|
||||
error = abs(error);
|
||||
if (error > 0 && error <= 7) return opus_errors[error - 1];
|
||||
return "undefined error";
|
||||
}
|
||||
|
||||
inline bool reinitialize_decoder(OpusHandle *handle) {
|
||||
int error;
|
||||
handle->decoder.reset(opus_decoder_create(handle->sampleRate, handle->channelCount, &error));
|
||||
if(error != OPUS_OK) {
|
||||
printf("Failed to create decoder (%s)\n", opus_error_message(error).data());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool reinitialize_encoder(OpusHandle *handle) {
|
||||
int error;
|
||||
handle->encoder.reset(opus_encoder_create(handle->sampleRate, handle->channelCount, handle->opusType, &error));
|
||||
if (error != OPUS_OK) {
|
||||
printf("Failed to create encoder (%s)\n", opus_error_message(error).data());
|
||||
return false;
|
||||
}
|
||||
|
||||
if(error = opus_encoder_ctl(&*handle->encoder, OPUS_SET_COMPLEXITY(1)); error != OPUS_OK) {
|
||||
printf("Failed to setup encoder (%s)\n", opus_error_message(error).data());
|
||||
return false;
|
||||
}
|
||||
//TODO: May set OPUS_SET_BITRATE(4740)?
|
||||
//TODO: Is the encoder event needed anymore? Or is it just overhead
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
struct OpusHandle {
|
||||
OpusEncoder* encoder = nullptr;
|
||||
OpusDecoder* decoder = nullptr;
|
||||
#endif
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
OpusHandle *codec_opus_createNativeHandle(size_t channelCount, int type) {
|
||||
auto codec = new OpusHandle{};
|
||||
codec->opusType = type;
|
||||
codec->channelCount = channelCount;
|
||||
codec->sampleRate = 48000;
|
||||
if (!reinitialize_decoder(codec)) return nullptr;
|
||||
if (!reinitialize_encoder(codec)) return nullptr;
|
||||
return codec;
|
||||
}
|
||||
|
||||
size_t channelCount = 1;
|
||||
size_t sampleRate = 48000;
|
||||
int opusType = OPUS_APPLICATION_AUDIO;
|
||||
};
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void codec_opus_deleteNativeHandle(OpusHandle *codec) {
|
||||
if (!codec) return;
|
||||
|
||||
const char* opus_errors[7] = {
|
||||
"One or more invalid/out of range arguments", //-1 (OPUS_BAD_ARG)
|
||||
"Not enough bytes allocated in the buffer", //-2 (OPUS_BUFFER_TOO_SMALL)
|
||||
"An internal error was detected", //-3 (OPUS_INTERNAL_ERROR)
|
||||
"The compressed data passed is corrupted", //-4 (OPUS_INVALID_PACKET)
|
||||
"Invalid/unsupported request number", //-5 (OPUS_UNIMPLEMENTED)
|
||||
"An encoder or decoder structure is invalid or already freed", //-6 (OPUS_INVALID_STATE)
|
||||
"Memory allocation has failed" //-7 (OPUS_ALLOC_FAIL)
|
||||
};
|
||||
codec->decoder.reset();
|
||||
codec->encoder.reset();
|
||||
delete codec;
|
||||
}
|
||||
|
||||
inline const char* opus_error_message(int error) {
|
||||
error = abs(error);
|
||||
if(error > 0 && error <= 7) return opus_errors[error - 1];
|
||||
return "undefined error";
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) {
|
||||
auto result = opus_encode_float(&*handle->encoder, (float *) buffer, length / handle->channelCount, buffer, maxLength);
|
||||
if (result < 0) return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline int currentMillies() {
|
||||
return EM_ASM_INT({ return Date.now(); });
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_decode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) {
|
||||
auto result = opus_decode_float(&*handle->decoder, buffer, length, (float *) buffer, maxLength / sizeof(float) / handle->channelCount, false);
|
||||
if (result < 0) return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
#define _S(x) #x
|
||||
#define INVOKE_OPUS(result, method, ...) \
|
||||
result = method( __VA_ARGS__ ); \
|
||||
if(error != 0){ \
|
||||
printf("Got opus error while invoking %s. Code: %d Message: %s\n", _S(method), error, opus_error_message(error)); \
|
||||
return false; \
|
||||
}
|
||||
|
||||
inline bool reinitialize_decoder(OpusHandle *handle) {
|
||||
if (handle->decoder)
|
||||
opus_decoder_destroy(handle->decoder);
|
||||
|
||||
int error = 0;
|
||||
INVOKE_OPUS(handle->decoder, opus_decoder_create, 48000, handle->channelCount, &error);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool reinitialize_encoder(OpusHandle *handle) {
|
||||
if (handle->encoder)
|
||||
opus_encoder_destroy(handle->encoder);
|
||||
|
||||
int error = 0;
|
||||
INVOKE_OPUS(handle->encoder, opus_encoder_create, 48000, handle->channelCount, handle->opusType, &error);
|
||||
INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_COMPLEXITY(1));
|
||||
//INVOKE_OPUS(error, opus_encoder_ctl, handle->encoder, OPUS_SET_BITRATE(4740));
|
||||
|
||||
EM_ASM(
|
||||
printMessageToServerTab('Encoder initialized!');
|
||||
printMessageToServerTab(' Comprexity: 1');
|
||||
printMessageToServerTab(' Bitrate: 4740');
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
OpusHandle* codec_opus_createNativeHandle(size_t channelCount, int type) {
|
||||
printf("Initialize opus. (Channel count: %d Sample rate: %d Type: %d)!\n", channelCount, 48000, type);
|
||||
auto codec = new OpusHandle{};
|
||||
codec->opusType = type;
|
||||
codec->channelCount = channelCount;
|
||||
if(!reinitialize_decoder(codec)) return nullptr;
|
||||
if(!reinitialize_encoder(codec)) return nullptr;
|
||||
return codec;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void codec_opus_deleteNativeHandle(OpusHandle* codec) {
|
||||
if(!codec) return;
|
||||
|
||||
if(codec->decoder) opus_decoder_destroy(codec->decoder);
|
||||
codec->decoder = nullptr;
|
||||
|
||||
if(codec->encoder) opus_encoder_destroy(codec->encoder);
|
||||
codec->encoder = nullptr;
|
||||
|
||||
delete codec;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_encode(OpusHandle* handle, uint8_t* buffer, size_t length, size_t maxLength) {
|
||||
auto begin = currentMillies();
|
||||
auto result = opus_encode_float(handle->encoder, (float*) buffer, length / handle->channelCount, buffer, maxLength);
|
||||
if(result < 0) return result;
|
||||
auto end = currentMillies();
|
||||
EM_ASM({
|
||||
printMessageToServerTab("codec_opus_encode(...) tooks " + $0 + "ms to execute!");
|
||||
}, end - begin);
|
||||
return result;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_decode(OpusHandle* handle, uint8_t* buffer, size_t length, size_t maxLength) {
|
||||
auto result = opus_decode_float(handle->decoder, buffer, length, (float*) buffer, maxLength / sizeof(float) / handle->channelCount, false);
|
||||
if(result < 0) return result; //Failed
|
||||
return result;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_changeApplication(OpusHandle* handle, int type) {
|
||||
handle->opusType = type;
|
||||
if(type != OPUS_APPLICATION_VOIP && type != OPUS_APPLICATION_AUDIO && type != OPUS_APPLICATION_RESTRICTED_LOWDELAY)
|
||||
return 1;
|
||||
return opus_encoder_ctl(handle->encoder, OPUS_SET_APPLICATION(type));
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_reset(OpusHandle* handle) {
|
||||
if(!reinitialize_decoder(handle)) return 0;
|
||||
if(!reinitialize_encoder(handle)) return 0;
|
||||
return 1;
|
||||
}
|
||||
/*
|
||||
opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
|
||||
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
|
||||
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type));
|
||||
*/
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int codec_opus_reset(OpusHandle *handle) {
|
||||
if (!reinitialize_decoder(handle)) return 0;
|
||||
if (!reinitialize_encoder(handle)) return 0;
|
||||
return 1;
|
||||
}
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,4 @@
|
|||
declare const __webpack_require__;
|
||||
window["shared-require"] = __webpack_require__;
|
||||
/* firstly assign the shared-require */
|
||||
setTimeout(() => require("tc-shared/main"), 0);
|
489
file.ts
|
@ -31,66 +31,19 @@ type ProjectResource = {
|
|||
const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
|
||||
{ /* shared html and php files */
|
||||
"type": "html",
|
||||
"search-pattern": /^([a-zA-Z]+)\.(html|php|json)$/,
|
||||
"search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "./",
|
||||
"local-path": "./shared/html/"
|
||||
},
|
||||
|
||||
{ /* javascript loader */
|
||||
{ /* javascript files as manifest.json */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"build-target": "dev",
|
||||
|
||||
"path": "loader/",
|
||||
"local-path": "./shared/loader/"
|
||||
},
|
||||
{ /* javascript loader for releases */
|
||||
"type": "js",
|
||||
"search-pattern": /.*loader_[\S]+.min.js$/,
|
||||
"build-target": "rel",
|
||||
|
||||
"path": "loader/",
|
||||
"local-path": "./shared/generated/"
|
||||
},
|
||||
|
||||
{ /* shared javascript files (WebRTC adapter) */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"search-pattern": /.*$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "adapter/",
|
||||
"local-path": "./shared/adapter/"
|
||||
},
|
||||
|
||||
{ /* shared javascript files (development mode only) */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"search-exclude": /(.*\/)?workers\/.*/,
|
||||
"build-target": "dev",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./shared/js/"
|
||||
},
|
||||
{ /* shared javascript mapping files (development mode only) */
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.(js.map|ts)$/,
|
||||
"search-exclude": /(.*\/)?workers\/.*/,
|
||||
"build-target": "dev",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./shared/js/",
|
||||
"req-parm": ["--mappings"]
|
||||
},
|
||||
|
||||
{ /* shared generated worker codec */
|
||||
"type": "js",
|
||||
"search-pattern": /(WorkerPOW.js)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "js/workers/",
|
||||
"local-path": "./shared/js/workers/"
|
||||
"local-path": "./dist/"
|
||||
},
|
||||
{ /* shared developer single css files */
|
||||
"type": "css",
|
||||
|
@ -156,14 +109,6 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
|
|||
|
||||
"path": "img/",
|
||||
"local-path": "./shared/img/"
|
||||
},
|
||||
{ /* own webassembly files */
|
||||
"type": "wasm",
|
||||
"search-pattern": /.*\.(wasm)/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "wat/",
|
||||
"local-path": "./shared/wat/"
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -204,28 +149,7 @@ const APP_FILE_LIST_CLIENT_SOURCE: ProjectResource[] = [
|
|||
|
||||
"path": "js/",
|
||||
"local-path": "./client/js/"
|
||||
},
|
||||
|
||||
/* release specific */
|
||||
{ /* web merged javascript files (shared inclusive) */
|
||||
"client-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"build-target": "rel",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./client/generated/"
|
||||
},
|
||||
{ /* Add the shared generated files. Exclude the shared file because we're including it already */
|
||||
"client-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"search-exclude": /shared\.js(.map)?$/,
|
||||
"build-target": "rel",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./shared/generated/"
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
||||
|
@ -238,42 +162,6 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
|||
"path": "wasm/",
|
||||
"local-path": "./asm/generated/"
|
||||
},
|
||||
{ /* generated assembly javascript files */
|
||||
"web-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.(js)/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "wasm/",
|
||||
"local-path": "./asm/generated/"
|
||||
},
|
||||
{ /* web generated worker codec */
|
||||
"web-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /(WorkerCodec.js)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "js/workers/",
|
||||
"local-path": "./web/js/workers/"
|
||||
},
|
||||
{ /* web javascript files (development mode only) */
|
||||
"web-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"build-target": "dev",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./web/js/"
|
||||
},
|
||||
{ /* web merged javascript files (shared inclusive) */
|
||||
"web-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /client(\.min)?\.js$/,
|
||||
"build-target": "rel",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./web/generated/"
|
||||
},
|
||||
{ /* web css files */
|
||||
"web-only": true,
|
||||
"type": "css",
|
||||
|
@ -303,6 +191,7 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
|||
}
|
||||
];
|
||||
|
||||
//TODO: This isn't needed anymore
|
||||
const APP_FILE_LIST_WEB_TEASPEAK: ProjectResource[] = [
|
||||
/* special web.teaspeak.de only auth files */
|
||||
{ /* login page and api */
|
||||
|
@ -347,6 +236,7 @@ const APP_FILE_LIST_WEB_TEASPEAK: ProjectResource[] = [
|
|||
}
|
||||
];
|
||||
|
||||
//FIXME: This isn't working right now
|
||||
const CERTACCEPT_FILE_LIST: ProjectResource[] = [
|
||||
{ /* html files */
|
||||
"type": "html",
|
||||
|
@ -438,15 +328,161 @@ const CERTACCEPT_FILE_LIST: ProjectResource[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const APP_FILE_LIST = [
|
||||
const CLIENT_APP_FILE_LIST = [
|
||||
...APP_FILE_LIST_SHARED_SOURCE,
|
||||
...APP_FILE_LIST_SHARED_VENDORS,
|
||||
...APP_FILE_LIST_CLIENT_SOURCE,
|
||||
...APP_FILE_LIST_WEB_SOURCE,
|
||||
...APP_FILE_LIST_WEB_TEASPEAK,
|
||||
...CERTACCEPT_FILE_LIST,
|
||||
...APP_FILE_LIST_CLIENT_SOURCE
|
||||
];
|
||||
|
||||
const WEB_APP_FILE_LIST = [
|
||||
...APP_FILE_LIST_SHARED_SOURCE,
|
||||
...APP_FILE_LIST_SHARED_VENDORS,
|
||||
...APP_FILE_LIST_WEB_SOURCE,
|
||||
...APP_FILE_LIST_WEB_TEASPEAK,
|
||||
...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>;
|
||||
|
@ -487,41 +523,49 @@ namespace generator {
|
|||
return result.digest("hex");
|
||||
}
|
||||
|
||||
const rreaddir = async p => {
|
||||
const result = [];
|
||||
try {
|
||||
const files = await fs.readdir(p);
|
||||
for(const file of files) {
|
||||
const file_path = path.join(p, file);
|
||||
|
||||
const info = await fs.stat(file_path);
|
||||
if(info.isDirectory()) {
|
||||
result.push(...await rreaddir(file_path));
|
||||
} else {
|
||||
result.push(file_path);
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
if(error.code === "ENOENT")
|
||||
return [];
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function file_matches_options(file: ProjectResource, options: SearchOptions) {
|
||||
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
|
||||
return false;
|
||||
|
||||
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
|
||||
return false;
|
||||
|
||||
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
|
||||
return false;
|
||||
|
||||
if(!file["build-target"].split("|").find(e => e === options.mode))
|
||||
return false;
|
||||
|
||||
return !(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())));
|
||||
}
|
||||
|
||||
export async function search_files(files: ProjectResource[], options: SearchOptions) : Promise<Entry[]> {
|
||||
const result: Entry[] = [];
|
||||
|
||||
const rreaddir = async p => {
|
||||
const result = [];
|
||||
try {
|
||||
const files = await fs.readdir(p);
|
||||
for(const file of files) {
|
||||
const file_path = path.join(p, file);
|
||||
|
||||
const info = await fs.stat(file_path);
|
||||
if(info.isDirectory()) {
|
||||
result.push(...await rreaddir(file_path));
|
||||
} else {
|
||||
result.push(file_path);
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
if(error.code === "ENOENT")
|
||||
return [];
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
for(const file of files) {
|
||||
if(typeof file["web-only"] === "boolean" && file["web-only"] && options.target !== "web")
|
||||
continue;
|
||||
if(typeof file["client-only"] === "boolean" && file["client-only"] && options.target !== "client")
|
||||
continue;
|
||||
if(typeof file["serve-only"] === "boolean" && file["serve-only"] && !options.serving)
|
||||
continue;
|
||||
if(!file["build-target"].split("|").find(e => e === options.mode))
|
||||
continue;
|
||||
if(Array.isArray(file["req-parm"]) && file["req-parm"].find(e => !options.parameter.find(p => p.toLowerCase() === e.toLowerCase())))
|
||||
if(!file_matches_options(file, options))
|
||||
continue;
|
||||
|
||||
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
||||
|
@ -549,23 +593,52 @@ namespace generator {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function search_http_file(files: ProjectResource[], target_file: string, options: SearchOptions) : Promise<string> {
|
||||
for(const file of files) {
|
||||
if(!file_matches_options(file, options))
|
||||
continue;
|
||||
|
||||
if(file.path !== "./" && !target_file.startsWith("/" + file.path.replace(/\\/g, "/")))
|
||||
continue;
|
||||
|
||||
const normal_local = path.normalize(path.join(options.source_path, file["local-path"]));
|
||||
const files: string[] = await rreaddir(normal_local);
|
||||
for(const f of files) {
|
||||
const local_name = f.substr(normal_local.length);
|
||||
if(!local_name.match(file["search-pattern"]) && !local_name.replace("\\\\", "/").match(file["search-pattern"]))
|
||||
continue;
|
||||
|
||||
if(typeof(file["search-exclude"]) !== "undefined" && f.match(file["search-exclude"]))
|
||||
continue;
|
||||
|
||||
if("/" + path.join(file.path, local_name).replace(/\\/g, "/") === target_file)
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
namespace server {
|
||||
import SearchOptions = generator.SearchOptions;
|
||||
export type Options = {
|
||||
port: number;
|
||||
php: string;
|
||||
|
||||
search_options: SearchOptions;
|
||||
}
|
||||
|
||||
const exec: (command: string) => Promise<{ stdout: string, stderr: string }> = util.promisify(cp.exec);
|
||||
|
||||
let files: (generator.Entry & { http_path: string; })[] = [];
|
||||
let files: ProjectResource[] = [];
|
||||
let server: http.Server;
|
||||
let php: string;
|
||||
export async function launch(_files: generator.Entry[], options: Options) {
|
||||
//Don't use this check anymore, because we're searching within the PATH variable
|
||||
//if(!await fs.exists(options.php) || !(await fs.stat(options.php)).isFile())
|
||||
// throw "invalid php interpreter (not found)";
|
||||
let options: Options;
|
||||
export async function launch(_files: ProjectResource[], options_: Options) {
|
||||
options = options_;
|
||||
files = _files;
|
||||
|
||||
try {
|
||||
const info = await exec(options.php + " --version");
|
||||
|
@ -589,17 +662,6 @@ namespace server {
|
|||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
files = _files.map(e =>{
|
||||
return {
|
||||
type: e.type,
|
||||
name: e.name,
|
||||
hash: e.hash,
|
||||
local_path: e.local_path,
|
||||
target_path: e.target_path,
|
||||
http_path: "/" + e.target_path.replace(/\\/g, "/")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
|
@ -643,8 +705,8 @@ namespace server {
|
|||
});
|
||||
}
|
||||
|
||||
function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||
const file = files.find(e => e.http_path === pathname);
|
||||
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||
const file = await generator.search_http_file(files, pathname, options.search_options);
|
||||
if(!file) {
|
||||
console.log("[SERVER] Client requested unknown file %s", pathname);
|
||||
response.writeHead(404);
|
||||
|
@ -653,13 +715,13 @@ namespace server {
|
|||
return;
|
||||
}
|
||||
|
||||
let type = mt.lookup(path.extname(file.local_path)) || "text/html";
|
||||
console.log("[SERVER] Serving file %s (%s) (%s)", file.target_path, type, file.local_path);
|
||||
if(path.extname(file.local_path) === ".php") {
|
||||
serve_php(file.local_path, query, response);
|
||||
let type = mt.lookup(path.extname(file)) || "text/html";
|
||||
console.log("[SERVER] Serving file %s", file, type);
|
||||
if(path.extname(file) === ".php") {
|
||||
serve_php(file, query, response);
|
||||
return;
|
||||
}
|
||||
const fis = fs.createReadStream(file.local_path);
|
||||
const fis = fs.createReadStream(file);
|
||||
|
||||
response.writeHead(200, "success", {
|
||||
"Content-Type": type + "; charset=utf-8"
|
||||
|
@ -672,23 +734,23 @@ namespace server {
|
|||
fis.on("data", data => response.write(data));
|
||||
}
|
||||
|
||||
function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
|
||||
async function handle_api_request(request: http.IncomingMessage, response: http.ServerResponse, url: url_utils.UrlWithParsedQuery) {
|
||||
if(url.query["type"] === "files") {
|
||||
response.writeHead(200, { "info-version": 1 });
|
||||
response.write("type\thash\tpath\tname\n");
|
||||
for(const file of files)
|
||||
if(file.http_path.endsWith(".php"))
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path, ".php") + ".html" + "\n");
|
||||
for(const file of await generator.search_files(files, options.search_options))
|
||||
if(file.name.endsWith(".php"))
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + path.basename(file.name, ".php") + ".html" + "\n");
|
||||
else
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.http_path) + "\t" + path.basename(file.http_path) + "\n");
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + file.name + "\n");
|
||||
response.end();
|
||||
return;
|
||||
} else if(url.query["type"] === "file") {
|
||||
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
||||
if(!p.startsWith("/")) p = "/" + p;
|
||||
if(p.endsWith(".html")) {
|
||||
const np = p.substr(0, p.length - 5) + ".php";
|
||||
if(files.find(e => e.http_path == np) && !files.find(e => e.http_path == p))
|
||||
p = np;
|
||||
const np = await generator.search_http_file(files, p.substr(0, p.length - 5) + ".php", options.search_options);
|
||||
if(np) p = p.substr(0, p.length - 5) + ".php";
|
||||
}
|
||||
serve_file(p, url.query, response);
|
||||
return;
|
||||
|
@ -718,6 +780,8 @@ namespace server {
|
|||
//Client API
|
||||
handle_api_request(request, response, url);
|
||||
return;
|
||||
} else if(url.pathname === "/") {
|
||||
url.pathname = "/index.php";
|
||||
}
|
||||
serve_file(url.pathname, url.query, response);
|
||||
}
|
||||
|
@ -865,17 +929,16 @@ function php_exe() : string {
|
|||
}
|
||||
|
||||
async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: number) {
|
||||
const files = await generator.search_files(APP_FILE_LIST, {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: mode,
|
||||
serving: true
|
||||
});
|
||||
|
||||
await server.launch(files, {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: mode,
|
||||
serving: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Server started on %d", port);
|
||||
|
@ -884,14 +947,6 @@ async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: n
|
|||
}
|
||||
|
||||
async function main_develop(node: boolean, target: "client" | "web", port: number, flags: string[]) {
|
||||
const files = await generator.search_files(APP_FILE_LIST, {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: "dev",
|
||||
serving: true
|
||||
});
|
||||
|
||||
const tscwatcher = new watcher.TSCWatcher();
|
||||
try {
|
||||
if(flags.indexOf("--no-tsc") == -1)
|
||||
|
@ -903,9 +958,16 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
|||
await sasswatcher.start();
|
||||
|
||||
try {
|
||||
await server.launch(files, {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
target: target,
|
||||
mode: "dev",
|
||||
serving: true
|
||||
}
|
||||
});
|
||||
} catch(error) {
|
||||
console.error("Failed to start server: %o", error instanceof Error ? error.message : error);
|
||||
|
@ -944,23 +1006,17 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
|||
}
|
||||
|
||||
async function git_tag() {
|
||||
const exec = util.promisify(cp.exec);
|
||||
|
||||
/* check if we've any uncommited changes */
|
||||
{
|
||||
let { stdout, stderr } = await exec("git diff-index HEAD -- . ':!asm/libraries/' ':!package-lock.json' ':!vendor/'");
|
||||
if(stderr) throw stderr;
|
||||
if(stdout) return "0000000";
|
||||
}
|
||||
|
||||
let { stdout, stderr } = await exec("git rev-parse --short HEAD");
|
||||
if(stderr) throw stderr;
|
||||
return stdout.substr(0, 7);
|
||||
const git_rev = fs.readFileSync(path.join(__dirname, ".git", "HEAD")).toString();
|
||||
let version;
|
||||
if(git_rev.indexOf("/") === -1)
|
||||
return git_rev.substr(0, 7);
|
||||
else
|
||||
return fs.readFileSync(path.join(__dirname, ".git", git_rev.substr(5).trim())).toString().substr(0, 7);
|
||||
}
|
||||
|
||||
async function main_generate(target: "client" | "web", mode: "rel" | "dev", dest_path: string, args: any[]) {
|
||||
const begin = Date.now();
|
||||
const files = await generator.search_files(APP_FILE_LIST, {
|
||||
const files = await generator.search_files(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
source_path: __dirname,
|
||||
parameter: args,
|
||||
target: target,
|
||||
|
@ -1144,7 +1200,7 @@ async function main(args: string[]) {
|
|||
}
|
||||
|
||||
/* proxy log for better format */
|
||||
const wrap_log = (original, prefix: string) => (message, ...args) => original(prefix + message.replace(/\n/g, "\n" + prefix), ...args.map(e => typeof(e) === "string" ? e.replace(/\n/g, "\n" + prefix) : e));
|
||||
const wrap_log = (original, prefix: string) => (message, ...args) => original(prefix + (message ? message + "" : "").replace(/\n/g, "\n" + prefix), ...args.map(e => typeof(e) === "string" ? e.replace(/\n/g, "\n" + prefix) : e));
|
||||
console.log = wrap_log(console.log, "[INFO ] ");
|
||||
console.debug = wrap_log(console.debug, "[DEBUG] ");
|
||||
console.warn = wrap_log(console.warn, "[WARNING] ");
|
||||
|
@ -1156,4 +1212,5 @@ main(process.argv.slice(2)).then(ignore_exit => {
|
|||
}).catch(error => {
|
||||
console.error("Failed to execute application. Exception reached execution root!");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import * as loader from "./targets/app";
|
||||
import * as loader_base from "./loader/loader";
|
||||
|
||||
window["loader"] = loader_base;
|
||||
/* let the loader register himself at the window first */
|
||||
setTimeout(loader.run, 0);
|
||||
|
||||
export {};
|
|
@ -0,0 +1,393 @@
|
|||
import * as script_loader from "./script_loader";
|
||||
import * as style_loader from "./style_loader";
|
||||
import * as template_loader from "./template_loader";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tr(message: string) : string;
|
||||
tra(message: string, ...args: any[]);
|
||||
|
||||
log: any;
|
||||
StaticSettings: any;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
loader_groups: boolean;
|
||||
verbose: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export let config: Config = {
|
||||
loader_groups: false,
|
||||
verbose: false,
|
||||
error: true
|
||||
};
|
||||
|
||||
export type Task = {
|
||||
name: string,
|
||||
priority: number, /* tasks with the same priority will be executed in sync */
|
||||
function: () => Promise<void>
|
||||
};
|
||||
|
||||
export enum Stage {
|
||||
/*
|
||||
loading loader required files (incl this)
|
||||
*/
|
||||
INITIALIZING,
|
||||
/*
|
||||
setting up the loading process
|
||||
*/
|
||||
SETUP,
|
||||
/*
|
||||
loading all style sheet files
|
||||
*/
|
||||
STYLE,
|
||||
/*
|
||||
loading all javascript files
|
||||
*/
|
||||
JAVASCRIPT,
|
||||
/*
|
||||
loading all template files
|
||||
*/
|
||||
TEMPLATES,
|
||||
/*
|
||||
initializing static/global stuff
|
||||
*/
|
||||
JAVASCRIPT_INITIALIZING,
|
||||
/*
|
||||
finalizing load process
|
||||
*/
|
||||
FINALIZING,
|
||||
/*
|
||||
invoking main task
|
||||
*/
|
||||
LOADED,
|
||||
|
||||
DONE
|
||||
}
|
||||
|
||||
let cache_tag: string | undefined;
|
||||
let current_stage: Stage = undefined;
|
||||
const tasks: {[key:number]:Task[]} = {};
|
||||
|
||||
/* test if all files shall be load from cache or fetch again */
|
||||
function loader_cache_tag() {
|
||||
if(__build.mode === "debug") {
|
||||
cache_tag = "?_ts=" + Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
const cached_version = localStorage.getItem("cached_version");
|
||||
if(!cached_version || cached_version !== __build.version) {
|
||||
register_task(Stage.LOADED, {
|
||||
priority: 0,
|
||||
name: "cached version updater",
|
||||
function: async () => {
|
||||
localStorage.setItem("cached_version", __build.version);
|
||||
}
|
||||
});
|
||||
}
|
||||
cache_tag = "?_version=" + __build.version;
|
||||
}
|
||||
|
||||
export type ModuleMapping = {
|
||||
application: string,
|
||||
modules: {
|
||||
"id": string,
|
||||
"context": string,
|
||||
"resource": string
|
||||
}[]
|
||||
};
|
||||
const module_mapping_: ModuleMapping[] = [];
|
||||
export function module_mapping() : ModuleMapping[] { return module_mapping_; }
|
||||
|
||||
export function get_cache_version() { return cache_tag; }
|
||||
|
||||
export function finished() {
|
||||
return current_stage == Stage.DONE;
|
||||
}
|
||||
export function running() { return typeof(current_stage) !== "undefined"; }
|
||||
|
||||
export function register_task(stage: Stage, task: Task) {
|
||||
if(current_stage > stage) {
|
||||
if(config.error)
|
||||
console.warn("Register loading task, but it had already been finished. Executing task anyways!");
|
||||
|
||||
const promise = task.function();
|
||||
if(!promise) {
|
||||
console.error("Loading task %s hasn't returned a promise!", task.name);
|
||||
return;
|
||||
}
|
||||
promise.catch(error => {
|
||||
if(config.error) {
|
||||
console.error("Failed to execute delayed loader task!");
|
||||
console.log(" - %s: %o", task.name, error);
|
||||
}
|
||||
|
||||
critical_error(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const task_array = tasks[stage] || [];
|
||||
task_array.push(task);
|
||||
tasks[stage] = task_array.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
export async function execute() {
|
||||
document.getElementById("loader-overlay").classList.add("started");
|
||||
loader_cache_tag();
|
||||
|
||||
const load_begin = Date.now();
|
||||
|
||||
let begin: number = 0;
|
||||
let end: number = Date.now();
|
||||
while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") {
|
||||
|
||||
let current_tasks: Task[] = [];
|
||||
while((tasks[current_stage] || []).length > 0) {
|
||||
if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) {
|
||||
current_tasks.push(tasks[current_stage].pop());
|
||||
} else break;
|
||||
}
|
||||
|
||||
const errors: {
|
||||
error: any,
|
||||
task: Task
|
||||
}[] = [];
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
for(const task of current_tasks) {
|
||||
try {
|
||||
if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority);
|
||||
const promise = task.function();
|
||||
if(!promise) {
|
||||
console.error("Loading task %s hasn't returned a promise!", task.name);
|
||||
continue;
|
||||
}
|
||||
promises.push(promise.catch(error => {
|
||||
errors.push({
|
||||
task: task,
|
||||
error: error
|
||||
});
|
||||
return Promise.resolve();
|
||||
}));
|
||||
} catch(error) {
|
||||
errors.push({
|
||||
task: task,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(promises.length > 0) {
|
||||
await Promise.all([...promises]);
|
||||
}
|
||||
|
||||
if(errors.length > 0) {
|
||||
if(config.loader_groups) console.groupEnd();
|
||||
console.error("Failed to execute loader. The following tasks failed (%d):", errors.length);
|
||||
for(const error of errors)
|
||||
console.error(" - %s: %o", error.task.name, error.error);
|
||||
|
||||
throw "failed to process step " + Stage[current_stage];
|
||||
}
|
||||
|
||||
if(current_tasks.length == 0) {
|
||||
if(typeof(current_stage) === "undefined") {
|
||||
current_stage = -1;
|
||||
if(config.verbose) console.debug("[loader] Booting app");
|
||||
} else if(current_stage < Stage.INITIALIZING) {
|
||||
if(config.loader_groups) console.groupEnd();
|
||||
if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin);
|
||||
} else {
|
||||
if(config.loader_groups) console.groupEnd();
|
||||
if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin);
|
||||
}
|
||||
|
||||
begin = end;
|
||||
current_stage += 1;
|
||||
|
||||
if(current_stage != Stage.DONE && config.loader_groups)
|
||||
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]);
|
||||
}
|
||||
}
|
||||
|
||||
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
|
||||
}
|
||||
export function execute_managed() {
|
||||
execute().then(() => {
|
||||
if(config.verbose) {
|
||||
let message;
|
||||
if(typeof(window.tr) !== "undefined")
|
||||
message = tr("App loaded successfully!");
|
||||
else
|
||||
message = "App loaded successfully!";
|
||||
|
||||
if(typeof(window.log) !== "undefined") {
|
||||
/* We're having our log module */
|
||||
window.log.info(window.log.LogCategory.GENERAL, message);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("App loading failed: %o", error);
|
||||
critical_error("Failed to execute loader", "Lookup the console for more detail");
|
||||
});
|
||||
}
|
||||
|
||||
let _fadeout_warned;
|
||||
export function hide_overlay() {
|
||||
if(typeof($) === "undefined") {
|
||||
if(!_fadeout_warned)
|
||||
console.warn("Could not fadeout loader screen. Missing jquery functions.");
|
||||
_fadeout_warned = true;
|
||||
return;
|
||||
}
|
||||
const animation_duration = 750;
|
||||
|
||||
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
|
||||
$(".loader .half").animate({width: 0}, animation_duration, () => {
|
||||
$(".loader").detach();
|
||||
});
|
||||
}
|
||||
|
||||
/* critical error handler */
|
||||
export type ErrorHandler = (message: string, detail: string) => void;
|
||||
let _callback_critical_error: ErrorHandler;
|
||||
let _callback_critical_called: boolean = false;
|
||||
export function critical_error(message: string, detail?: string) {
|
||||
document.getElementById("loader-overlay").classList.add("started");
|
||||
|
||||
if(_callback_critical_called) {
|
||||
console.warn("[CRITICAL] %s", message);
|
||||
if(typeof(detail) === "string")
|
||||
console.warn("[CRITICAL] %s", detail);
|
||||
return;
|
||||
}
|
||||
|
||||
_callback_critical_called = true;
|
||||
if(_callback_critical_error) {
|
||||
_callback_critical_error(message, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
/* default handling */
|
||||
let tag = document.getElementById("critical-load");
|
||||
|
||||
{
|
||||
const error_tags = tag.getElementsByClassName("error");
|
||||
error_tags[0].innerHTML = message;
|
||||
}
|
||||
|
||||
if(typeof(detail) === "string") {
|
||||
let node_detail = tag.getElementsByClassName("detail")[0];
|
||||
node_detail.innerHTML = detail;
|
||||
}
|
||||
|
||||
tag.classList.add("shown");
|
||||
}
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler {
|
||||
if((typeof(handler) === "object" && handler !== _callback_critical_error) || override)
|
||||
_callback_critical_error = handler;
|
||||
return _callback_critical_error;
|
||||
}
|
||||
|
||||
/* loaders */
|
||||
export type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
}
|
||||
export type SourcePath = string | DependSource | string[];
|
||||
|
||||
export const scripts = script_loader;
|
||||
export const style = style_loader;
|
||||
export const templates = template_loader;
|
||||
|
||||
/* Hello World message */
|
||||
{
|
||||
|
||||
const hello_world = () => {
|
||||
const clog = console.log;
|
||||
const print_security = () => {
|
||||
{
|
||||
const css = [
|
||||
"display: block",
|
||||
"text-align: center",
|
||||
"font-size: 42px",
|
||||
"font-weight: bold",
|
||||
"-webkit-text-stroke: 2px black",
|
||||
"color: red"
|
||||
].join(";");
|
||||
clog("%c ", "font-size: 100px;");
|
||||
clog("%cSecurity warning:", css);
|
||||
}
|
||||
{
|
||||
const css = [
|
||||
"display: block",
|
||||
"text-align: center",
|
||||
"font-size: 18px",
|
||||
"font-weight: bold"
|
||||
].join(";");
|
||||
|
||||
clog("%cPasting anything in here could give attackers access to your data.", css);
|
||||
clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css);
|
||||
clog("%c ", "font-size: 100px;");
|
||||
}
|
||||
};
|
||||
|
||||
/* print the hello world */
|
||||
{
|
||||
const css = [
|
||||
"display: block",
|
||||
"text-align: center",
|
||||
"font-size: 72px",
|
||||
"font-weight: bold",
|
||||
"-webkit-text-stroke: 2px black",
|
||||
"color: #18BC9C"
|
||||
].join(";");
|
||||
clog("%cHey, hold on!", css);
|
||||
}
|
||||
{
|
||||
const css = [
|
||||
"display: block",
|
||||
"text-align: center",
|
||||
"font-size: 26px",
|
||||
"font-weight: bold"
|
||||
].join(";");
|
||||
|
||||
const css_2 = [
|
||||
"display: block",
|
||||
"text-align: center",
|
||||
"font-size: 26px",
|
||||
"font-weight: bold",
|
||||
"color: blue"
|
||||
].join(";");
|
||||
|
||||
const display_detect = /./;
|
||||
display_detect.toString = function() { print_security(); return ""; };
|
||||
|
||||
clog("%cLovely to see you using and debugging the TeaSpeak-Web client.", css);
|
||||
clog("%cIf you have some good ideas or already done some incredible changes,", css);
|
||||
clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2);
|
||||
clog("%c ", display_detect);
|
||||
}
|
||||
};
|
||||
|
||||
try { /* lets try to print it as VM code :)*/
|
||||
let hello_world_code = hello_world.toString();
|
||||
hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8);
|
||||
hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}"));
|
||||
|
||||
//Look aheads are not possible with firefox
|
||||
//hello_world_code = hello_world_code.replace(/(?<!const|let)(?<=^([^"'/]|"[^"]*"|'[^']*'|`[^`]*`|\/[^/]*\/)*) /gm, ""); /* replace all spaces */
|
||||
hello_world_code = hello_world_code.replace(/[\n\r]/g, ""); /* replace as new lines */
|
||||
|
||||
eval(hello_world_code);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
hello_world();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import {config, critical_error, SourcePath} from "./loader";
|
||||
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||
|
||||
let _script_promises: {[key: string]: Promise<void>} = {};
|
||||
|
||||
function load_script_url(url: string) : Promise<void> {
|
||||
if(typeof _script_promises[url] === "object")
|
||||
return _script_promises[url];
|
||||
|
||||
return (_script_promises[url] = new Promise((resolve, reject) => {
|
||||
const script_tag: HTMLScriptElement = document.createElement("script");
|
||||
|
||||
let error = false;
|
||||
const error_handler = (event: ErrorEvent) => {
|
||||
if(event.filename == script_tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error
|
||||
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
|
||||
reject(new LoadSyntaxError(event.error));
|
||||
event.preventDefault();
|
||||
error = true;
|
||||
}
|
||||
};
|
||||
window.addEventListener('error', error_handler as any);
|
||||
|
||||
const cleanup = () => {
|
||||
script_tag.onerror = undefined;
|
||||
script_tag.onload = undefined;
|
||||
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
};
|
||||
const timeout_handle = setTimeout(() => {
|
||||
cleanup();
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
script_tag.type = "application/javascript";
|
||||
script_tag.async = true;
|
||||
script_tag.defer = true;
|
||||
script_tag.onerror = error => {
|
||||
cleanup();
|
||||
script_tag.remove();
|
||||
reject(error);
|
||||
};
|
||||
script_tag.onload = () => {
|
||||
cleanup();
|
||||
|
||||
if(config.verbose) console.debug("Script %o loaded", url);
|
||||
setTimeout(resolve, 100);
|
||||
};
|
||||
|
||||
document.getElementById("scripts").appendChild(script_tag);
|
||||
|
||||
script_tag.src = url;
|
||||
})).then(result => {
|
||||
/* cleanup memory */
|
||||
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||
return _script_promises[url];
|
||||
}).catch(error => {
|
||||
/* cleanup memory */
|
||||
_script_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||
return _script_promises[url];
|
||||
});
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
cache_tag?: string;
|
||||
}
|
||||
|
||||
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||
if(Array.isArray(path)) { //We have fallback scripts
|
||||
return load(path[0], options).catch(error => {
|
||||
if(error instanceof LoadSyntaxError)
|
||||
return Promise.reject(error);
|
||||
|
||||
if(path.length > 1)
|
||||
return load(path.slice(1), options);
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
} else {
|
||||
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||
if(source.url.length == 0) return Promise.resolve();
|
||||
|
||||
/* await depends */
|
||||
for(const depend of source.depends) {
|
||||
if(!_script_promises[depend])
|
||||
throw "Missing dependency " + depend;
|
||||
await _script_promises[depend];
|
||||
}
|
||||
await load_script_url(source.url + (options.cache_tag || ""));
|
||||
}
|
||||
}
|
||||
|
||||
type MultipleOptions = Options | ParallelOptions;
|
||||
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||
if(result.failed.length > 0) {
|
||||
if(config.error) {
|
||||
console.error("Failed to load the following scripts:");
|
||||
for(const script of result.failed) {
|
||||
const sname = script_name(script.request, false);
|
||||
if(script.error instanceof LoadSyntaxError) {
|
||||
const source = script.error.source as Error;
|
||||
if(source.name === "TypeError") {
|
||||
let prefix = "";
|
||||
while(prefix.length < sname.length + 7) prefix += " ";
|
||||
console.log(" - %s: %s:\n%s", sname, source.message, source.stack.split("\n").map(e => prefix + e.trim()).slice(1).join("\n"));
|
||||
} else {
|
||||
console.log(" - %s: %o", sname, source);
|
||||
}
|
||||
} else {
|
||||
console.log(" - %s: %o", sname, script.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
critical_error("Failed to load script " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||
throw "failed to load script " + script_name(result.failed[0].request, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import {config, critical_error, SourcePath} from "./loader";
|
||||
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||
|
||||
let _style_promises: {[key: string]: Promise<void>} = {};
|
||||
|
||||
function load_style_url(url: string) : Promise<void> {
|
||||
if(typeof _style_promises[url] === "object")
|
||||
return _style_promises[url];
|
||||
|
||||
return (_style_promises[url] = new Promise((resolve, reject) => {
|
||||
const tag: HTMLLinkElement = document.createElement("link");
|
||||
|
||||
let error = false;
|
||||
const error_handler = (event: ErrorEvent) => {
|
||||
if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error);
|
||||
if(event.filename == tag.href) { //FIXME!
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
|
||||
reject(new SyntaxError(event.error));
|
||||
event.preventDefault();
|
||||
error = true;
|
||||
}
|
||||
};
|
||||
window.addEventListener('error', error_handler as any);
|
||||
|
||||
tag.type = "text/css";
|
||||
tag.rel = "stylesheet";
|
||||
|
||||
const cleanup = () => {
|
||||
tag.onerror = undefined;
|
||||
tag.onload = undefined;
|
||||
|
||||
clearTimeout(timeout_handle);
|
||||
window.removeEventListener('error', error_handler as any);
|
||||
};
|
||||
|
||||
const timeout_handle = setTimeout(() => {
|
||||
cleanup();
|
||||
reject("timeout");
|
||||
}, 5000);
|
||||
|
||||
tag.onerror = error => {
|
||||
cleanup();
|
||||
tag.remove();
|
||||
if(config.error)
|
||||
console.error("File load error for file %s: %o", url, error);
|
||||
reject("failed to load file " + url);
|
||||
};
|
||||
tag.onload = () => {
|
||||
cleanup();
|
||||
{
|
||||
const css: CSSStyleSheet = tag.sheet as CSSStyleSheet;
|
||||
const rules = css.cssRules;
|
||||
const rules_remove: number[] = [];
|
||||
const rules_add: string[] = [];
|
||||
|
||||
for(let index = 0; index < rules.length; index++) {
|
||||
const rule = rules.item(index);
|
||||
let rule_text = rule.cssText;
|
||||
|
||||
if(rule.cssText.indexOf("%%base_path%%") != -1) {
|
||||
rules_remove.push(index);
|
||||
rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname));
|
||||
}
|
||||
}
|
||||
|
||||
for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) {
|
||||
if(css.removeRule)
|
||||
css.removeRule(index);
|
||||
else
|
||||
css.deleteRule(index);
|
||||
}
|
||||
for(const rule of rules_add)
|
||||
css.insertRule(rule, rules_remove[0]);
|
||||
}
|
||||
|
||||
if(config.verbose) console.debug("Style sheet %o loaded", url);
|
||||
setTimeout(resolve, 100);
|
||||
};
|
||||
|
||||
document.getElementById("style").appendChild(tag);
|
||||
tag.href = url;
|
||||
})).then(result => {
|
||||
/* cleanup memory */
|
||||
_style_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||
return _style_promises[url];
|
||||
}).catch(error => {
|
||||
/* cleanup memory */
|
||||
_style_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||
return _style_promises[url];
|
||||
});
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
cache_tag?: string;
|
||||
}
|
||||
|
||||
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||
if(Array.isArray(path)) { //We have fallback scripts
|
||||
return load(path[0], options).catch(error => {
|
||||
if(error instanceof LoadSyntaxError)
|
||||
return Promise.reject(error);
|
||||
|
||||
if(path.length > 1)
|
||||
return load(path.slice(1), options);
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
} else {
|
||||
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||
if(source.url.length == 0) return Promise.resolve();
|
||||
|
||||
/* await depends */
|
||||
for(const depend of source.depends) {
|
||||
if(!_style_promises[depend])
|
||||
throw "Missing dependency " + depend;
|
||||
await _style_promises[depend];
|
||||
}
|
||||
await load_style_url(source.url + (options.cache_tag || ""));
|
||||
}
|
||||
}
|
||||
|
||||
export type MultipleOptions = Options | ParallelOptions;
|
||||
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||
if(result.failed.length > 0) {
|
||||
if(config.error) {
|
||||
console.error("Failed to load the following style sheets:");
|
||||
for(const style of result.failed) {
|
||||
const sname = script_name(style.request, false);
|
||||
if(style.error instanceof LoadSyntaxError) {
|
||||
console.log(" - %s: %o", sname, style.error.source);
|
||||
} else {
|
||||
console.log(" - %s: %o", sname, style.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
critical_error("Failed to load style " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||
throw "failed to load style " + script_name(result.failed[0].request, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import {config, critical_error, SourcePath} from "./loader";
|
||||
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
|
||||
|
||||
let _template_promises: {[key: string]: Promise<void>} = {};
|
||||
|
||||
function load_template_url(url: string) : Promise<void> {
|
||||
if(typeof _template_promises[url] === "object")
|
||||
return _template_promises[url];
|
||||
|
||||
return (_template_promises[url] = (async () => {
|
||||
const response = await $.ajax(url);
|
||||
|
||||
let node = document.createElement("html");
|
||||
node.innerHTML = response;
|
||||
let tags: HTMLCollection;
|
||||
if(node.getElementsByTagName("body").length > 0)
|
||||
tags = node.getElementsByTagName("body")[0].children;
|
||||
else
|
||||
tags = node.children;
|
||||
|
||||
let root = document.getElementById("templates");
|
||||
if(!root) {
|
||||
critical_error("Failed to find template tag!");
|
||||
throw "Failed to find template tag";
|
||||
}
|
||||
while(tags.length > 0){
|
||||
let tag = tags.item(0);
|
||||
root.appendChild(tag);
|
||||
|
||||
}
|
||||
})()).then(result => {
|
||||
/* cleanup memory */
|
||||
_template_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||
return _template_promises[url];
|
||||
}).catch(error => {
|
||||
/* cleanup memory */
|
||||
_template_promises[url] = Promise.reject(error); /* this promise does not holds the whole script tag and other memory */
|
||||
return _template_promises[url];
|
||||
});
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
cache_tag?: string;
|
||||
}
|
||||
|
||||
export async function load(path: SourcePath, options: Options) : Promise<void> {
|
||||
if(Array.isArray(path)) { //We have fallback scripts
|
||||
return load(path[0], options).catch(error => {
|
||||
if(error instanceof LoadSyntaxError)
|
||||
return Promise.reject(error);
|
||||
|
||||
if(path.length > 1)
|
||||
return load(path.slice(1), options);
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
} else {
|
||||
const source = typeof(path) === "string" ? {url: path, depends: []} : path;
|
||||
if(source.url.length == 0) return Promise.resolve();
|
||||
|
||||
/* await depends */
|
||||
for(const depend of source.depends) {
|
||||
if(!_template_promises[depend])
|
||||
throw "Missing dependency " + depend;
|
||||
await _template_promises[depend];
|
||||
}
|
||||
await load_template_url(source.url + (options.cache_tag || ""));
|
||||
}
|
||||
}
|
||||
|
||||
export type MultipleOptions = Options | ParallelOptions;
|
||||
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> {
|
||||
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options);
|
||||
if(result.failed.length > 0) {
|
||||
if(config.error) {
|
||||
console.error("Failed to load the following template files:");
|
||||
for(const style of result.failed) {
|
||||
const sname = script_name(style.request, false);
|
||||
if(style.error instanceof LoadSyntaxError) {
|
||||
console.log(" - %s: %o", sname, style.error.source);
|
||||
} else {
|
||||
console.log(" - %s: %o", sname, style.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
critical_error("Failed to load template file " + script_name(result.failed[0].request, true) + " <br>" + "View the browser console for more information!");
|
||||
throw "failed to load template file " + script_name(result.failed[0].request, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import {SourcePath} from "./loader";
|
||||
import {Options} from "./script_loader";
|
||||
|
||||
export class LoadSyntaxError {
|
||||
readonly source: any;
|
||||
constructor(source: any) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
export function script_name(path: SourcePath, html: boolean) {
|
||||
if(Array.isArray(path)) {
|
||||
let buffer = "";
|
||||
let _or = " or ";
|
||||
for(let entry of path)
|
||||
buffer += _or + script_name(entry, html);
|
||||
return buffer.slice(_or.length);
|
||||
} else if(typeof(path) === "string")
|
||||
return html ? "<code>" + path + "</code>" : path;
|
||||
else
|
||||
return html ? "<code>" + path.url + "</code>" : path.url;
|
||||
}
|
||||
|
||||
export interface ParallelOptions extends Options {
|
||||
max_parallel_requests?: number
|
||||
}
|
||||
|
||||
export interface ParallelResult<T> {
|
||||
succeeded: T[];
|
||||
failed: {
|
||||
request: T,
|
||||
error: T
|
||||
}[],
|
||||
|
||||
skipped: T[];
|
||||
}
|
||||
|
||||
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions) : Promise<ParallelResult<T>> {
|
||||
const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] };
|
||||
const pending_requests = requests.slice(0).reverse(); /* we're only able to pop from the back */
|
||||
const current_requests = {};
|
||||
|
||||
while (pending_requests.length > 0) {
|
||||
while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(current_requests).length < options.max_parallel_requests) {
|
||||
const script = pending_requests.pop();
|
||||
const name = stringify(script);
|
||||
|
||||
current_requests[name] = executor(script).catch(e => result.failed.push({ request: script, error: e })).then(() => {
|
||||
delete current_requests[name];
|
||||
});
|
||||
if(pending_requests.length == 0) break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wait 'till a new "slot" for downloading is free.
|
||||
* This should also not throw because any errors will be caught before.
|
||||
*/
|
||||
await Promise.race(Object.keys(current_requests).map(e => current_requests[e]));
|
||||
if(result.failed.length > 0)
|
||||
break; /* finish loading the other requests and than show the error */
|
||||
}
|
||||
await Promise.all(Object.keys(current_requests).map(e => current_requests[e]));
|
||||
result.skipped.push(...pending_requests);
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,415 @@
|
|||
import * as loader from "../loader/loader";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
native_client: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const node_require: typeof require = window.require;
|
||||
|
||||
function cache_tag() {
|
||||
const ui = ui_version();
|
||||
return "?_ts=" + (!!ui && ui !== "unknown" ? ui : Date.now());
|
||||
}
|
||||
|
||||
let _ui_version;
|
||||
export function ui_version() {
|
||||
if(typeof(_ui_version) !== "string") {
|
||||
const version_node = document.getElementById("app_version");
|
||||
if(!version_node) return undefined;
|
||||
|
||||
const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined;
|
||||
if(!version) return undefined;
|
||||
|
||||
return (_ui_version = version);
|
||||
}
|
||||
return _ui_version;
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
version: number;
|
||||
|
||||
chunks: {[key: string]: {
|
||||
files: {
|
||||
hash: string,
|
||||
file: string
|
||||
}[],
|
||||
modules: {
|
||||
id: string,
|
||||
context: string,
|
||||
resource: string
|
||||
}[]
|
||||
}};
|
||||
}
|
||||
|
||||
/* all javascript loaders */
|
||||
const loader_javascript = {
|
||||
load_scripts: async () => {
|
||||
if(!window.require) {
|
||||
await loader.scripts.load(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() });
|
||||
} else {
|
||||
/*
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "forum sync",
|
||||
priority: 10,
|
||||
function: async () => {
|
||||
forum.sync_main();
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
await loader.scripts.load_multiple([
|
||||
["vendor/jsrender/jsrender.min.js"],
|
||||
["vendor/xbbcode/src/parser.js"],
|
||||
["vendor/emoji-picker/src/jquery.lsxemojipicker.js"],
|
||||
["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */
|
||||
["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */
|
||||
["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
|
||||
let manifest: Manifest;
|
||||
try {
|
||||
const response = await fetch("js/manifest.json");
|
||||
if(!response.ok) throw response.status + " " + response.statusText;
|
||||
|
||||
manifest = await response.json();
|
||||
} catch(error) {
|
||||
console.error("Failed to load javascript manifest: %o", error);
|
||||
loader.critical_error("Failed to load manifest.json", error);
|
||||
throw "failed to load manifest.json";
|
||||
}
|
||||
if(manifest.version !== 2)
|
||||
throw "invalid manifest version";
|
||||
|
||||
const chunk_name = __build.entry_chunk_name;
|
||||
if(typeof manifest.chunks[chunk_name] !== "object") {
|
||||
loader.critical_error("Missing entry chunk in manifest.json", "Chunk " + chunk_name + " is missing.");
|
||||
throw "missing entry chunk";
|
||||
}
|
||||
loader.module_mapping().push({
|
||||
application: chunk_name,
|
||||
modules: manifest.chunks[chunk_name].modules
|
||||
});
|
||||
await loader.scripts.load_multiple(manifest.chunks[chunk_name].files.map(e => "js/" + e.file), {
|
||||
cache_tag: undefined,
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loader_webassembly = {
|
||||
test_webassembly: async () => {
|
||||
/* We dont required WebAssembly anymore for fundamental functions, only for auto decoding
|
||||
if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") {
|
||||
console.log(navigator.browserSpecs);
|
||||
if (navigator.browserSpecs.name == 'Safari') {
|
||||
if (parseInt(navigator.browserSpecs.version) < 11) {
|
||||
displayCriticalError("You require Safari 11 or higher to use the web client!<br>Safari " + navigator.browserSpecs.version + " does not support WebAssambly!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Do something for all other browsers.
|
||||
}
|
||||
displayCriticalError("You require WebAssembly for TeaSpeak-Web!");
|
||||
throw "Missing web assembly";
|
||||
}
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
const loader_style = {
|
||||
load_style: async () => {
|
||||
const options = {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
};
|
||||
|
||||
await loader.style.load_multiple([
|
||||
"vendor/xbbcode/src/xbbcode.css"
|
||||
], options);
|
||||
await loader.style.load_multiple([
|
||||
"vendor/emoji-picker/src/jquery.lsxemojipicker.css"
|
||||
], options);
|
||||
await loader.style.load_multiple([
|
||||
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
|
||||
], options);
|
||||
|
||||
if(__build.mode === "debug") {
|
||||
await loader_style.load_style_debug();
|
||||
} else {
|
||||
await loader_style.load_style_release();
|
||||
}
|
||||
},
|
||||
|
||||
load_style_debug: async () => {
|
||||
await loader.style.load_multiple([
|
||||
"css/static/main.css",
|
||||
"css/static/main-layout.css",
|
||||
"css/static/helptag.css",
|
||||
"css/static/scroll.css",
|
||||
"css/static/channel-tree.css",
|
||||
"css/static/ts/tab.css",
|
||||
"css/static/ts/chat.css",
|
||||
"css/static/ts/icons.css",
|
||||
"css/static/ts/icons_em.css",
|
||||
"css/static/ts/country.css",
|
||||
"css/static/general.css",
|
||||
"css/static/modal.css",
|
||||
"css/static/modals.css",
|
||||
"css/static/modal-about.css",
|
||||
"css/static/modal-avatar.css",
|
||||
"css/static/modal-icons.css",
|
||||
"css/static/modal-bookmarks.css",
|
||||
"css/static/modal-connect.css",
|
||||
"css/static/modal-channel.css",
|
||||
"css/static/modal-query.css",
|
||||
"css/static/modal-volume.css",
|
||||
"css/static/modal-latency.css",
|
||||
"css/static/modal-invite.css",
|
||||
"css/static/modal-playlist.css",
|
||||
"css/static/modal-banlist.css",
|
||||
"css/static/modal-banclient.css",
|
||||
"css/static/modal-channelinfo.css",
|
||||
"css/static/modal-clientinfo.css",
|
||||
"css/static/modal-serverinfo.css",
|
||||
"css/static/modal-musicmanage.css",
|
||||
"css/static/modal-serverinfobandwidth.css",
|
||||
"css/static/modal-identity.css",
|
||||
"css/static/modal-newcomer.css",
|
||||
"css/static/modal-settings.css",
|
||||
"css/static/modal-poke.css",
|
||||
"css/static/modal-server.css",
|
||||
"css/static/modal-keyselect.css",
|
||||
"css/static/modal-permissions.css",
|
||||
"css/static/modal-group-assignment.css",
|
||||
"css/static/overlay-image-preview.css",
|
||||
"css/static/music/info_plate.css",
|
||||
"css/static/frame/SelectInfo.css",
|
||||
"css/static/control_bar.css",
|
||||
"css/static/context_menu.css",
|
||||
"css/static/frame-chat.css",
|
||||
"css/static/connection_handlers.css",
|
||||
"css/static/server-log.css",
|
||||
"css/static/htmltags.css",
|
||||
"css/static/hostbanner.css",
|
||||
"css/static/menu-bar.css"
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
},
|
||||
|
||||
load_style_release: async () => {
|
||||
await loader.style.load_multiple([
|
||||
"css/static/base.css",
|
||||
"css/static/main.css",
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* register tasks */
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "safari fix",
|
||||
function: async () => {
|
||||
/* safari remove "fix" */
|
||||
if(Element.prototype.remove === undefined)
|
||||
Object.defineProperty(Element.prototype, "remove", {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function(){
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
});
|
||||
},
|
||||
priority: 50
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "Browser detection",
|
||||
function: async () => {
|
||||
navigator.browserSpecs = (function(){
|
||||
let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
||||
if(/trident/i.test(M[1])){
|
||||
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
||||
return {name:'IE',version:(tem[1] || '')};
|
||||
}
|
||||
if(M[1]=== 'Chrome'){
|
||||
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
|
||||
if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]};
|
||||
}
|
||||
M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
|
||||
if((tem = ua.match(/version\/(\d+)/i))!= null)
|
||||
M.splice(1, 1, tem[1]);
|
||||
return {name:M[0], version:M[1]};
|
||||
})();
|
||||
|
||||
console.log("Resolved browser manufacturer to \"%s\" version \"%s\"", navigator.browserSpecs.name, navigator.browserSpecs.version);
|
||||
},
|
||||
priority: 30
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "secure tester",
|
||||
function: async () => {
|
||||
/* we need https or localhost to use some things like the storage API */
|
||||
if(typeof isSecureContext === "undefined")
|
||||
(<any>window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost';
|
||||
|
||||
if(!isSecureContext) {
|
||||
loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!");
|
||||
throw "App requires a secure context!"
|
||||
}
|
||||
},
|
||||
priority: 20
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "webassembly tester",
|
||||
function: loader_webassembly.test_webassembly,
|
||||
priority: 20
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "javascript",
|
||||
function: loader_javascript.load_scripts,
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.STYLE, {
|
||||
name: "style",
|
||||
function: loader_style.load_style,
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.TEMPLATES, {
|
||||
name: "templates",
|
||||
function: async () => {
|
||||
await loader.templates.load_multiple([
|
||||
"templates.html",
|
||||
"templates/modal/musicmanage.html",
|
||||
"templates/modal/newcomer.html",
|
||||
], {
|
||||
cache_tag: cache_tag(),
|
||||
max_parallel_requests: -1
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
name: "loaded handler",
|
||||
function: async () => loader.hide_overlay(),
|
||||
priority: 5
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "lsx emoji picker setup",
|
||||
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
|
||||
priority: 10
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.SETUP, {
|
||||
name: "page setup",
|
||||
function: async () => {
|
||||
const body = document.body;
|
||||
/* top menu */
|
||||
{
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "top-menu-bar");
|
||||
body.append(container);
|
||||
}
|
||||
/* template containers */
|
||||
{
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "templates");
|
||||
body.append(container);
|
||||
}
|
||||
/* sounds container */
|
||||
{
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "sounds");
|
||||
body.append(container);
|
||||
}
|
||||
/* mouse move container */
|
||||
{
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "mouse-move");
|
||||
|
||||
const inner_container = document.createElement("div");
|
||||
inner_container.classList.add("container");
|
||||
container.append(inner_container);
|
||||
|
||||
body.append(container);
|
||||
}
|
||||
/* tooltip container */
|
||||
{
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "global-tooltip");
|
||||
|
||||
container.append(document.createElement("a"));
|
||||
|
||||
body.append(container);
|
||||
}
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
||||
/* test if we're getting loaded within a TeaClient preview window */
|
||||
loader.register_task(loader.Stage.SETUP, {
|
||||
name: "TeaClient tester",
|
||||
function: async () => {
|
||||
//@ts-ignore
|
||||
if(typeof __teaclient_preview_notice !== "undefined" && typeof __teaclient_preview_error !== "undefined") {
|
||||
loader.critical_error("Why you're opening TeaWeb within the TeaSpeak client?!");
|
||||
throw "we're already a TeaClient!";
|
||||
}
|
||||
},
|
||||
priority: 100
|
||||
});
|
||||
|
||||
export function run() {
|
||||
window["Module"] = (window["Module"] || {}) as any; /* Why? */
|
||||
|
||||
/* TeaClient */
|
||||
if(node_require) {
|
||||
if(__build.target !== "client") {
|
||||
loader.critical_error("App seems not to be compiled for the client.", "This app has been compiled for " + __build.target);
|
||||
return;
|
||||
}
|
||||
window.native_client = true;
|
||||
|
||||
const path = node_require("path");
|
||||
const remote = node_require('electron').remote;
|
||||
|
||||
const render_entry = path.join(remote.app.getAppPath(), "/modules/", "renderer");
|
||||
const render = node_require(render_entry);
|
||||
|
||||
loader.register_task(loader.Stage.INITIALIZING, {
|
||||
name: "teaclient initialize",
|
||||
function: render.initialize,
|
||||
priority: 40
|
||||
});
|
||||
} else {
|
||||
if(__build.target !== "web") {
|
||||
loader.critical_error("App seems not to be compiled for the web.", "This app has been compiled for " + __build.target);
|
||||
return;
|
||||
}
|
||||
|
||||
window.native_client = false;
|
||||
}
|
||||
|
||||
if(!loader.running()) {
|
||||
/* we know that we want to load the app */
|
||||
loader.execute_managed();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/// <reference path="loader.ts" />
|
||||
import * as loader from "../loader/loader";
|
||||
|
||||
let is_debug = false;
|
||||
|
||||
|
@ -25,34 +25,8 @@ const loader_javascript = {
|
|||
|
||||
load_scripts: async () => {
|
||||
await loader.load_script(["vendor/jquery/jquery.min.js"]);
|
||||
|
||||
if(!is_debug) {
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "scripts release",
|
||||
priority: 20,
|
||||
function: loader_javascript.load_release
|
||||
});
|
||||
} else {
|
||||
loader.register_task(loader.Stage.JAVASCRIPT, {
|
||||
name: "scripts debug",
|
||||
priority: 20,
|
||||
function: loader_javascript.load_scripts_debug
|
||||
});
|
||||
}
|
||||
},
|
||||
load_scripts_debug: async () => {
|
||||
await loader.load_scripts([
|
||||
["js/proto.js"],
|
||||
["js/log.js"],
|
||||
["js/BrowserIPC.js"],
|
||||
["js/settings.js"],
|
||||
["js/main.js"]
|
||||
]);
|
||||
},
|
||||
|
||||
load_release: async () => {
|
||||
await loader.load_scripts([
|
||||
["js/certaccept.min.js", "js/certaccept.js"]
|
||||
["dist/certificate-popup.js"],
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
@ -99,9 +73,7 @@ loader.register_task(loader.Stage.STYLE, {
|
|||
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
name: "loaded handler",
|
||||
function: async () => {
|
||||
fadeoutLoader();
|
||||
},
|
||||
function: async () => loader.hide_overlay(),
|
||||
priority: 0
|
||||
});
|
||||
|
||||
|
@ -123,25 +95,6 @@ loader.register_task(loader.Stage.INITIALIZING, {
|
|||
priority: 50
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "settings initialisation",
|
||||
function: async () => Settings.initialize(),
|
||||
priority: 200
|
||||
});
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "bipc initialisation",
|
||||
function: async () => bipc.setup(),
|
||||
priority: 100
|
||||
});
|
||||
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "log enabled initialisation",
|
||||
function: async () => log.initialize(is_debug ? log.LogType.TRACE : log.LogType.INFO),
|
||||
priority: 150
|
||||
});
|
||||
|
||||
if(!loader.running()) {
|
||||
/* we know that we want to load the app */
|
||||
loader.execute_managed();
|
|
@ -0,0 +1,87 @@
|
|||
export interface Config {
|
||||
loader_groups: boolean;
|
||||
verbose: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export enum BackendType {
|
||||
WEB,
|
||||
NATIVE
|
||||
}
|
||||
|
||||
export interface AppVersion {
|
||||
ui: string;
|
||||
backend: string;
|
||||
|
||||
type: "web" | "native";
|
||||
debug_mode: boolean;
|
||||
}
|
||||
|
||||
export let config: Config;
|
||||
|
||||
export type Task = {
|
||||
name: string,
|
||||
priority: number, /* tasks with the same priority will be executed in sync */
|
||||
function: () => Promise<void>
|
||||
};
|
||||
export enum Stage {
|
||||
/*
|
||||
loading loader required files (incl this)
|
||||
*/
|
||||
INITIALIZING,
|
||||
/*
|
||||
setting up the loading process
|
||||
*/
|
||||
SETUP,
|
||||
/*
|
||||
loading all style sheet files
|
||||
*/
|
||||
STYLE,
|
||||
/*
|
||||
loading all javascript files
|
||||
*/
|
||||
JAVASCRIPT,
|
||||
/*
|
||||
loading all template files
|
||||
*/
|
||||
TEMPLATES,
|
||||
/*
|
||||
initializing static/global stuff
|
||||
*/
|
||||
JAVASCRIPT_INITIALIZING,
|
||||
/*
|
||||
finalizing load process
|
||||
*/
|
||||
FINALIZING,
|
||||
/*
|
||||
invoking main task
|
||||
*/
|
||||
LOADED,
|
||||
|
||||
DONE
|
||||
}
|
||||
|
||||
export type ModuleMapping = {
|
||||
application: string,
|
||||
modules: {
|
||||
"id": string,
|
||||
"context": string,
|
||||
"resource": string
|
||||
}[]
|
||||
};
|
||||
export function module_mapping() : ModuleMapping;
|
||||
|
||||
export function finished();
|
||||
export function running();
|
||||
export function register_task(stage: Stage, task: Task);
|
||||
export function execute() : Promise<void>;
|
||||
export function execute_managed();
|
||||
export type DependSource = {
|
||||
url: string;
|
||||
depends: string[];
|
||||
}
|
||||
export type SourcePath = string | DependSource | string[];
|
||||
export type ErrorHandler = (message: string, detail: string) => void;
|
||||
export function critical_error(message: string, detail?: string);
|
||||
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
|
||||
export function hide_overlay();
|
51
package.json
|
@ -5,41 +5,62 @@
|
|||
"main": "main.js",
|
||||
"directories": {},
|
||||
"scripts": {
|
||||
"compile-sass": "sass --update .:.",
|
||||
"compile-file-helper": "tsc file.ts",
|
||||
"build-worker-codec": "tsc -p web/js/workers/tsconfig_worker_codec.json",
|
||||
"build-worker-pow": "tsc -p shared/js/workers/tsconfig_worker_pow.json",
|
||||
"build-worker": "npm run build-worker-codec; npm run build-worker-pow;",
|
||||
"compile-sass": "sass --update shared/css/:shared/css/ web/css/:web/css/ client/css/:client/css/",
|
||||
"compile-project-base": "tsc -p tsbaseconfig.json",
|
||||
"dtsgen": "node tools/dtsgen/index.js",
|
||||
"trgen": "node tools/trgen/index.js",
|
||||
"ttsc": "ttsc",
|
||||
"sass": "sass",
|
||||
"csso": "csso",
|
||||
"rebuild-structure-web-dev": "php files.php generate web dev",
|
||||
"minify-web-rel-file": "terser --compress --mangle --ecma 6 --keep_classnames --keep_fnames --output",
|
||||
"start": "node file.js ndevelop"
|
||||
"start": "npm run compile-file-helper && node file.js ndevelop",
|
||||
"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"
|
||||
},
|
||||
"author": "TeaSpeak (WolverinDEV)",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^2.0.1",
|
||||
"@types/emscripten": "^1.38.0",
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/node": "^12.7.2",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/sha256": "^0.2.0",
|
||||
"@types/websocket": "0.0.40",
|
||||
"chunk-manifest-webpack-plugin": "^1.1.2",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"clean-css": "^4.2.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^3.4.2",
|
||||
"csso-cli": "^2.0.2",
|
||||
"exports-loader": "^0.7.0",
|
||||
"fs-extra": "latest",
|
||||
"gulp": "^4.0.2",
|
||||
"html-loader": "^1.0.0",
|
||||
"html-webpack-plugin": "^4.0.3",
|
||||
"mime-types": "^2.1.24",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-sass": "^4.13.1",
|
||||
"raw-loader": "^4.0.0",
|
||||
"sass": "1.22.10",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sha256": "^0.2.0",
|
||||
"style-loader": "^1.1.3",
|
||||
"terser": "^4.2.1",
|
||||
"ttypescript": "^1.5.7",
|
||||
"typescript": "3.5.3",
|
||||
"wat2wasm": "^1.0.2",
|
||||
"fs-extra": "latest"
|
||||
"terser-webpack-plugin": "latest",
|
||||
"ts-loader": "^6.2.2",
|
||||
"typescript": "3.6.5",
|
||||
"wabt": "^1.0.13",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"worker-plugin": "^4.0.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -50,6 +71,10 @@
|
|||
},
|
||||
"homepage": "https://www.teaspeak.de",
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^8.0.1"
|
||||
"dompurify": "^2.0.8",
|
||||
"moment": "^2.24.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"webrtc-adapter": "^7.5.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$(dirname "$0")/resolve_commands.sh"
|
||||
cd "$(dirname "$0")/../" || { echo "Failed to enter the base directory"; exit 1; }
|
||||
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Invalid argument count!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "client" ]]; then
|
||||
build_target="client"
|
||||
elif [[ "$1" == "web" ]]; then
|
||||
build_target="web"
|
||||
else
|
||||
echo "Invalid option $2"
|
||||
echo 'Available options are: "web" or "client"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$2" == "development" ]] || [[ "$2" == "dev" ]] || [[ "$2" == "debug" ]]; then
|
||||
build_type="development"
|
||||
elif [[ "$2" == "release" ]] || [[ "$2" == "rel" ]]; then
|
||||
build_type="release"
|
||||
else
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Invalid argument count!"
|
||||
else
|
||||
echo "Invalid option $2"
|
||||
fi
|
||||
echo 'Available options are: "development" or "dev", "release" or "rel"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating required project build files"
|
||||
npm run compile-project-base; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to generate project build files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating required build tooks"
|
||||
./tools/build_trgen.sh; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build build_typescript translation generator"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating style files"
|
||||
npm run compile-sass; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to generate style files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compile vendor XBBCode"
|
||||
execute_ttsc -p ./vendor/xbbcode/tsconfig.json; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build the XBBCode vendor"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$build_type" == "release" ]]; then # Compile everything for release mode
|
||||
echo "Packing generated css files"
|
||||
./shared/css/generate_packed.sh; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to package generated css files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_ENV=production npm run build-$build_target; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build the $build_target applcation"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$build_type" == "development" ]]; then
|
||||
NODE_ENV=development npm run build-$build_target; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build the $build_target applcation"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Generating environment"
|
||||
node file.js generate $build_target ${build_type}; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to generate environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$build_target builded successfully!"
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
# shellcheck disable=SC1090
|
||||
source "${BASEDIR}/resolve_commands.sh"
|
||||
cd "$BASEDIR/../"
|
||||
cd "$BASEDIR/../" || { echo "Failed to enter parent directory!"; exit 1; }
|
||||
|
||||
function generate_link() {
|
||||
if [[ ! -L $2 ]] || [[ "${BASH_ARGV[0]}" == "force" ]]; then
|
||||
|
@ -20,12 +21,19 @@ function replace_tribble() {
|
|||
|
||||
|
||||
#Building the generator
|
||||
./tools/build_dtsgen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build typescript declaration generator"
|
||||
./tools/build_dtsgen.sh; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to build typescript declaration generator ($_exit_code)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Shared
|
||||
./shared/generate_declarations.sh; _exit_code=$?
|
||||
[[ $_exit_code -ne 0 ]] && {
|
||||
echo "Failed to generate shared ($_exit_code)"
|
||||
}
|
||||
|
||||
exit 0
|
||||
#Easy going: Each "module" has it's exports and imports
|
||||
#So lets first build the exports and ignore any errors
|
||||
#Note: For the client we have to use the given file
|
||||
|
@ -40,12 +48,6 @@ npm run dtsgen -- --config client/tsconfig/dtsconfig.json -v
|
|||
replace_tribble client/declarations/exports.d.ts
|
||||
echo "Generated client declarations"
|
||||
|
||||
#Shared
|
||||
./shared/generate_declarations.sh
|
||||
[[ $? -ne 0 ]] && {
|
||||
echo "Failed to generate shared"
|
||||
}
|
||||
|
||||
#replace_tribble shared/declarations/exports.d.ts
|
||||
echo "Generated shared declarations"
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ function remove_if_exists() {
|
|||
function cleanup_declarations() {
|
||||
remove_if_exists shared/declarations/
|
||||
remove_if_exists web/declarations/
|
||||
remove_if_exists client/declarations/
|
||||
}
|
||||
|
||||
function cleanup_generated_files() {
|
||||
|
@ -58,4 +59,4 @@ cleanup_declarations
|
|||
echo "Deleting generated output files"
|
||||
cleanup_generated_files
|
||||
|
||||
echo "Project cleaned up"
|
||||
echo "Project cleaned up"
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
source `dirname $0`/resolve_commands.sh
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR/../"
|
||||
|
||||
source_path="client-api/environment/ui-files/raw"
|
||||
if [[ "$1" == "development" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "debug" ]]; then
|
||||
type="development"
|
||||
elif [[ "$1" == "release" ]] || [[ "$1" == "rel" ]]; then
|
||||
type="release"
|
||||
else
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Invalid argument count!"
|
||||
else
|
||||
echo "Invalid option $1"
|
||||
fi
|
||||
echo 'Available options are: "development" or "dev", "release" or "rel"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating file helper script"
|
||||
npm run compile-file-helper
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate file helper"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating style files"
|
||||
npm run compile-sass
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate style files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating web workers"
|
||||
npm run build-worker-codec
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web worker codec"
|
||||
exit 1
|
||||
fi
|
||||
npm run build-worker-pow
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web worker pow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Lets build some tools
|
||||
#dtsgen should be already build by build_declarations.sh
|
||||
./tools/build_trgen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build typescript translation generator"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Now lets build the declarations
|
||||
echo "Building declarations"
|
||||
./scripts/build_declarations.sh force
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate declarations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$type" == "release" ]]; then #Compile everything for release mode
|
||||
#Compile the shared source first
|
||||
echo "Building shared source"
|
||||
./shared/generate_packed.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build shared source"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Now compile the web client itself
|
||||
echo "Building client UI"
|
||||
./client/generate_packed.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web client"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$type" == "development" ]]; then
|
||||
echo "Building shared source"
|
||||
execute_ttsc -p ./shared/tsconfig/tsconfig.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to compile shared sources"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building client UI source"
|
||||
execute_ttsc -p ./client/tsconfig/tsconfig.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to compile web sources"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Generating environment"
|
||||
node file.js generate client ${type}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully build!"
|
|
@ -1,12 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ./scripts/deploy_ui_files.sh http://dev.clientapi.teaspeak.de/api.php test 1.1.0
|
||||
# Example usage: ./scripts/deploy_ui_files.sh http://dev.clientapi.teaspeak.de/api.php test 1.1.0
|
||||
|
||||
TMP_FILE_NAME="TeaSpeakUI.tar.gz"
|
||||
TMP_DIR_NAME="tmp"
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR/../"
|
||||
cd "$(dirname "$0")/../" || { echo "failed to enter base directory"; exit 1; }
|
||||
|
||||
if [[ "$#" -ne 3 ]]; then
|
||||
echo "Illegal number of parameters (url | channel | required version)"
|
||||
|
@ -18,6 +17,7 @@ if [[ ! -d client-api/environment/ui-files/ ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2154
|
||||
if [[ "${teaclient_deploy_secret}" == "" ]]; then
|
||||
echo "Missing deploy secret!"
|
||||
exit 1
|
||||
|
@ -26,33 +26,36 @@ fi
|
|||
if [[ -e "${TMP_FILE_NAME}" ]]; then
|
||||
echo "Temp file already exists!"
|
||||
echo "Deleting it!"
|
||||
rm ${TMP_FILE_NAME}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
|
||||
if ! rm ${TMP_FILE_NAME}; then
|
||||
echo "Failed to delete file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
GIT_HASH=$(git rev-parse --verify --short HEAD)
|
||||
APPLICATION_VERSION=$(cat package.json | python -c "import sys, json; print(json.load(sys.stdin)['version'])")
|
||||
APPLICATION_VERSION=$(< package.json python -c "import sys, json; print(json.load(sys.stdin)['version'])")
|
||||
echo "Git hash ${GIT_HASH} on version ${APPLICATION_VERSION} on channel $2"
|
||||
|
||||
#Packaging the app
|
||||
cd client-api/environment/ui-files/
|
||||
cd client-api/environment/ui-files/ || {
|
||||
echo "Missing UI files directory"
|
||||
exit 1
|
||||
}
|
||||
if [[ -e ${TMP_DIR_NAME} ]]; then
|
||||
rm -r ${TMP_DIR_NAME}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
if ! rm -r ${TMP_DIR_NAME}; then
|
||||
echo "Failed to remove temporary directory!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cp -rL raw ${TMP_DIR_NAME}
|
||||
|
||||
for file in $(find ${TMP_DIR_NAME} -name '*.php'); do
|
||||
while IFS= read -r -d '' file
|
||||
do
|
||||
echo "Evaluating php file $file"
|
||||
__cur_dir=$(pwd)
|
||||
cd $(dirname ${file})
|
||||
RESULT=$(php "$(basename ${file})" 2> /dev/null)
|
||||
cd "$(dirname "${file}")" || { echo "Failed to enter php file directory"; exit 1; }
|
||||
php_result=$(php "$(basename "${file}")" 2> /dev/null)
|
||||
CODE=$?
|
||||
if [[ ${CODE} -ne 0 ]]; then
|
||||
echo "Failed to evaluate php file $file!"
|
||||
|
@ -60,14 +63,14 @@ for file in $(find ${TMP_DIR_NAME} -name '*.php'); do
|
|||
exit 1
|
||||
fi
|
||||
|
||||
cd ${__cur_dir}
|
||||
echo "${RESULT}" > "${file::-4}.html"
|
||||
done
|
||||
cd "${__cur_dir}" || { echo "failed to enter original dir"; exit 1; }
|
||||
echo "${php_result}" > "${file::-4}.html"
|
||||
done < <(find "${TMP_DIR_NAME}" -name '*.php' -print0)
|
||||
|
||||
cd ${TMP_DIR_NAME}
|
||||
tar chvzf ${TMP_FILE_NAME} *
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to pack file"
|
||||
cd ${TMP_DIR_NAME} || { echo "failed to enter the temp dir"; exit 1; }
|
||||
tar chvzf ${TMP_FILE_NAME} ./*; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to pack file ($_exit_code)"
|
||||
exit 1
|
||||
fi
|
||||
mv ${TMP_FILE_NAME} ../../../../
|
||||
|
@ -84,16 +87,16 @@ RESP=$(curl \
|
|||
-F "version=$APPLICATION_VERSION" \
|
||||
-F "git_ref=$GIT_HASH" \
|
||||
-F "secret=${teaclient_deploy_secret}" \
|
||||
-F "file=@`pwd`/TeaSpeakUI.tar.gz" \
|
||||
$1
|
||||
-F "file=@$(pwd)/TeaSpeakUI.tar.gz" \
|
||||
"$1"
|
||||
)
|
||||
echo "$RESP"
|
||||
SUCCESS=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['success'])")
|
||||
SUCCESS=$(echo "${RESP}" | python -c "import sys, json; print(json.load(sys.stdin)['success'])")
|
||||
|
||||
if [[ ! "${SUCCESS}" == "True" ]]; then
|
||||
ERROR=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['error'])" 2>/dev/null)
|
||||
if [[ $? -ne 0 ]]; then
|
||||
ERROR=$(echo ${RESP} | python -c "import sys, json; print(json.load(sys.stdin)['msg'])" 2>/dev/null)
|
||||
ERROR=$(echo "${RESP}" | python -c "import sys, json; print(json.load(sys.stdin)['error'])" 2>/dev/null); _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
ERROR=$(echo "${RESP}" | python -c "import sys, json; print(json.load(sys.stdin)['msg'])" 2>/dev/null)
|
||||
fi
|
||||
echo "Failed to deploy build!"
|
||||
echo "${ERROR}"
|
||||
|
|
|
@ -14,13 +14,13 @@ if [[ "$response" != "" ]]; then
|
|||
exit 1
|
||||
else
|
||||
if [[ "$1" == "sort-tag" ]]; then
|
||||
echo "$(git rev-parse --short HEAD)"
|
||||
git rev-parse --short HEAD
|
||||
fi
|
||||
if [[ "$1" == "name" ]]; then
|
||||
echo "$(git rev-parse --short HEAD)"
|
||||
git rev-parse --short HEAD
|
||||
fi
|
||||
if [[ "$1" == "file-name" ]]; then
|
||||
echo "$(git rev-parse --short HEAD)"
|
||||
git rev-parse --short HEAD
|
||||
fi
|
||||
exit 0
|
||||
fi
|
|
@ -1,117 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
source `dirname $0`/resolve_commands.sh
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR/../"
|
||||
|
||||
if [[ "$1" == "development" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "debug" ]]; then
|
||||
source_path="web/environment/development"
|
||||
type="development"
|
||||
elif [[ "$1" == "release" ]] || [[ "$1" == "rel" ]]; then
|
||||
source_path="web/environment/release"
|
||||
type="release"
|
||||
else
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Invalid argument count!"
|
||||
else
|
||||
echo "Invalid option $1"
|
||||
fi
|
||||
echo 'Available options are: "development" or "dev", "release" or "rel"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating file helper script"
|
||||
npm run compile-file-helper
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate file helper"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compile vendor XBBCode"
|
||||
execute_ttsc -p ./vendor/xbbcode/tsconfig.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build the XBBCode vendor"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
execute_ttsc ./vendor/emoji-picker/src/jquery.lsxemojipicker.ts
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build the lsxemojipicker vendor"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating style files"
|
||||
npm run compile-sass
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate style files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating web workers"
|
||||
npm run build-worker-codec
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web worker codec"
|
||||
exit 1
|
||||
fi
|
||||
npm run build-worker-pow
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web worker pow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Lets build some tools
|
||||
#dtsgen should be already build by build_declarations.sh
|
||||
./tools/build_trgen.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build typescript translation generator"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Now lets build the declarations
|
||||
echo "Building declarations"
|
||||
./scripts/build_declarations.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate declarations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$type" == "release" ]]; then #Compile everything for release mode
|
||||
#Compile the shared source first
|
||||
echo "Building shared source"
|
||||
./shared/generate_packed.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build shared source"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#Now compile the web client itself
|
||||
echo "Building web client"
|
||||
./web/generate_packed.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web client"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$type" == "development" ]]; then
|
||||
echo "Building shared source"
|
||||
execute_ttsc -p ./shared/tsconfig/tsconfig.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to compile shared sources"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building web client source"
|
||||
execute_ttsc -p ./web/tsconfig/tsconfig.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to compile web sources"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Generating environment"
|
||||
node file.js generate web ${type}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully build!"
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR/../"
|
||||
cd "$(dirname "$0")/../" || { echo "Failed to enter base directory"; exit 1; }
|
||||
|
||||
if [[ "$1" == "development" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "dev" ]]; then
|
||||
source_path="web/environment/development"
|
||||
|
@ -36,19 +35,18 @@ fi
|
|||
|
||||
if [[ -e ${NAME} ]]; then
|
||||
echo "Found old file. Deleting it."
|
||||
rm -r ${NAME}
|
||||
rm -r "${NAME}"
|
||||
fi
|
||||
|
||||
current_path=$(pwd)
|
||||
cd "$source_path"
|
||||
zip -9 -r ${NAME} *
|
||||
cd "$source_path" || { echo "Failed to enter source path"; exit 1; }
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
if zip -9 -r "${NAME}" ./*; then
|
||||
echo "Failed to package environment!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$current_path"
|
||||
cd "$current_path" || { echo "Failed to reenter source path"; exit 1; }
|
||||
|
||||
mv "${source_path}/${NAME}" .
|
||||
echo "Release package successfully packaged!"
|
||||
|
|
|
@ -66,7 +66,7 @@ npm install
|
|||
Before you could start ahead with developing you've to compile everything.
|
||||
Just execute the `web_build.sh` script:
|
||||
```shell script
|
||||
./scripts/web_build.sh develop
|
||||
./scripts/web_build.sh development
|
||||
```
|
||||
|
||||
### 2.5 Starting the development environment
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import {Device} from "tc-shared/audio/player";
|
||||
|
||||
export function initialize() : boolean;
|
||||
export function initialized() : boolean;
|
||||
|
||||
export function get_master_volume() : number;
|
||||
export function set_master_volume(volume: number);
|
||||
|
||||
export function on_ready(cb: () => any);
|
||||
|
||||
export function available_devices() : Promise<Device[]>;
|
||||
export function set_device(device_id: string) : Promise<void>;
|
||||
export function current_device() : Device;
|
||||
|
||||
export function initializeFromGesture();
|
|
@ -0,0 +1,9 @@
|
|||
import {AbstractInput, InputDevice, LevelMeter} from "tc-shared/voice/RecorderBase";
|
||||
|
||||
export function devices() : InputDevice[];
|
||||
|
||||
export function device_refresh_available() : boolean;
|
||||
export function refresh_devices() : Promise<void>;
|
||||
|
||||
export function create_input() : AbstractInput;
|
||||
export function create_levelmeter(device: InputDevice) : Promise<LevelMeter>;
|
|
@ -0,0 +1,3 @@
|
|||
import {SoundFile} from "tc-shared/sound/Sounds";
|
||||
|
||||
export function play_sound(file: SoundFile) : Promise<void>;
|
|
@ -0,0 +1,5 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
|
||||
export function destroy_server_connection(handle: AbstractServerConnection);
|
|
@ -0,0 +1,5 @@
|
|||
import {AddressTarget, ResolveOptions} from "tc-shared/dns";
|
||||
import {ServerAddress} from "tc-shared/ui/server";
|
||||
|
||||
export function supported();
|
||||
export function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise<AddressTarget>;
|
|
@ -0,0 +1,12 @@
|
|||
import {KeyEvent, KeyHook, SpecialKey} from "tc-shared/PPTListener";
|
||||
|
||||
export function initialize() : Promise<void>;
|
||||
export function finalize(); // most the times not really required
|
||||
|
||||
export function register_key_listener(listener: (_: KeyEvent) => any);
|
||||
export function unregister_key_listener(listener: (_: KeyEvent) => any);
|
||||
|
||||
export function register_key_hook(hook: KeyHook);
|
||||
export function unregister_key_hook(hook: KeyHook);
|
||||
|
||||
export function key_pressed(code: string | SpecialKey) : boolean;
|
|
@ -1,31 +0,0 @@
|
|||
declare namespace audio {
|
||||
export namespace player {
|
||||
export function initialize() : boolean;
|
||||
export function initialized() : boolean;
|
||||
|
||||
export function context() : AudioContext;
|
||||
export function get_master_volume() : number;
|
||||
export function set_master_volume(volume: number);
|
||||
|
||||
export function destination() : AudioNode;
|
||||
|
||||
export function on_ready(cb: () => any);
|
||||
|
||||
export function available_devices() : Promise<Device[]>;
|
||||
export function set_device(device_id: string) : Promise<void>;
|
||||
|
||||
export function current_device() : Device;
|
||||
|
||||
export function initializeFromGesture();
|
||||
}
|
||||
|
||||
export namespace recorder {
|
||||
export function devices() : InputDevice[];
|
||||
|
||||
export function device_refresh_available() : boolean;
|
||||
export function refresh_devices() : Promise<void>;
|
||||
|
||||
export function create_input() : AbstractInput;
|
||||
export function create_levelmeter(device: InputDevice) : Promise<LevelMeter>;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
declare namespace connection {
|
||||
export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection;
|
||||
export function destroy_server_connection(handle: AbstractServerConnection);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
declare namespace ppt {
|
||||
export function initialize() : Promise<void>;
|
||||
export function finalize(); // most the times not really required
|
||||
|
||||
export function register_key_listener(listener: (_: KeyEvent) => any);
|
||||
export function unregister_key_listener(listener: (_: KeyEvent) => any);
|
||||
|
||||
export function register_key_hook(hook: KeyHook);
|
||||
export function unregister_key_hook(hook: KeyHook);
|
||||
|
||||
export function key_pressed(code: string | SpecialKey) : boolean;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
cd $(dirname $0)
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
#find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css
|
||||
|
||||
|
||||
|
@ -35,6 +35,7 @@ files=(
|
|||
"css/static/modal-group-assignment.css"
|
||||
"css/static/modal-icons.css"
|
||||
"css/static/modal-identity.css"
|
||||
"css/static/modal-newcomer.css"
|
||||
"css/static/modal-invite.css"
|
||||
"css/static/modal-keyselect.css"
|
||||
"css/static/modal-permissions.css"
|
||||
|
@ -42,10 +43,12 @@ files=(
|
|||
"css/static/modal-poke.css"
|
||||
"css/static/modal-query.css"
|
||||
"css/static/modal-server.css"
|
||||
"css/static/modal-musicmanage.css"
|
||||
"css/static/modal-serverinfobandwidth.css"
|
||||
"css/static/modal-serverinfo.css"
|
||||
"css/static/modal-settings.css"
|
||||
"css/static/modal-volume.css"
|
||||
"css/static/overlay-image-preview.css"
|
||||
|
||||
"css/static/ts/tab.css"
|
||||
"css/static/ts/chat.css"
|
||||
|
@ -59,20 +62,20 @@ files=(
|
|||
target_file=`pwd`/../generated/static/base.css
|
||||
|
||||
if [[ ! -d $(dirname ${target_file}) ]]; then
|
||||
echo "Creating target path ($(dirname ${target_file}))"
|
||||
mkdir -p $(dirname ${target_file})
|
||||
echo "Creating target path ($(dirname "${target_file}"))"
|
||||
mkdir -p $(dirname "${target_file}")
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to create target path!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "/* Auto generated merged CSS file */" > ${target_file}
|
||||
echo "/* Auto generated merged CSS file */" > "${target_file}"
|
||||
for file in "${files[@]}"; do
|
||||
if [[ ${file} =~ css/* ]]; then
|
||||
file="./${file:4}"
|
||||
fi
|
||||
cat ${file} >> ${target_file}
|
||||
cat "${file}" >> "${target_file}"
|
||||
done
|
||||
|
||||
cat ${target_file} | npm run csso -- --output `pwd`/../generated/static/base.css
|
||||
cat "${target_file}" | npm run csso -- --output "$(pwd)/../generated/static/base.css"
|
|
@ -4,6 +4,8 @@
|
|||
//$color_client_normal: #bebebe;
|
||||
$color_client_normal: #cccccc;
|
||||
$client_info_avatar_size: 10em;
|
||||
$bot_thumbnail_width: 16em;
|
||||
$bot_thumbnail_height: 9em;
|
||||
|
||||
.container-chat-frame {
|
||||
flex-grow: 1;
|
||||
|
@ -43,6 +45,8 @@ $client_info_avatar_size: 10em;
|
|||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 3.25em;
|
||||
|
||||
.block, .button {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -153,6 +157,10 @@ $client_info_avatar_size: 10em;
|
|||
&.chat-counter {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.bot-add-song {
|
||||
color: #3f7538;
|
||||
}
|
||||
}
|
||||
|
||||
.small-value {
|
||||
|
@ -185,6 +193,35 @@ $client_info_avatar_size: 10em;
|
|||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.mode-channel_chat) {
|
||||
.mode-channel_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-private_chat) {
|
||||
.mode-private_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-client_info) {
|
||||
.mode-client_info { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-music_bot) {
|
||||
.mode-music_bot { display: none; }
|
||||
}
|
||||
|
||||
&.mode-music_bot {
|
||||
.mode-music_bot {
|
||||
&.right {
|
||||
margin-left: 8.5em;
|
||||
}
|
||||
&.left {
|
||||
margin-right: 8.5em;
|
||||
}
|
||||
|
||||
width: 60em; /* same width so flex-shrik applies equaly */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -868,6 +905,13 @@ $client_info_avatar_size: 10em;
|
|||
line-height: 1.1em;
|
||||
|
||||
word-wrap: break-word;
|
||||
|
||||
.htmltag-client, .htmltag-channel {
|
||||
display: inline;
|
||||
|
||||
font-weight: bold;
|
||||
color: $color_client_normal;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
@ -1133,7 +1177,7 @@ $client_info_avatar_size: 10em;
|
|||
.container-property {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.icon_em {
|
||||
.icon_em, .container-icon {
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
|
@ -1146,7 +1190,7 @@ $client_info_avatar_size: 10em;
|
|||
text-align: left;
|
||||
|
||||
.container-property {
|
||||
.icon_em {
|
||||
.icon_em, .container-icon {
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
|
@ -1161,7 +1205,7 @@ $client_info_avatar_size: 10em;
|
|||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
> .icon_em {
|
||||
> .icon_em, > .container-icon {
|
||||
margin-top: .1em;
|
||||
margin-bottom: .1em;
|
||||
|
||||
|
@ -1169,6 +1213,14 @@ $client_info_avatar_size: 10em;
|
|||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.list {
|
||||
|
@ -1388,6 +1440,509 @@ $client_info_avatar_size: 10em;
|
|||
background-color: #25252a;
|
||||
}
|
||||
}
|
||||
|
||||
.xbbcode-tag-img {
|
||||
padding: .25em;
|
||||
border-radius: .25em;
|
||||
|
||||
overflow: hidden;
|
||||
max-width: 20em;
|
||||
max-height: 20em;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-music-info {
|
||||
position: relative;
|
||||
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
|
||||
.player {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.container-thumbnail {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: inline-block;
|
||||
margin: calc(#{$bot_thumbnail_height} / -2) .75em .5em .5em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
border-radius: .5em;
|
||||
overflow: hidden;
|
||||
|
||||
.thumbnail {
|
||||
overflow: hidden;
|
||||
|
||||
width: $bot_thumbnail_width;
|
||||
height: $bot_thumbnail_height;
|
||||
|
||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-song-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
min-width: 1em;
|
||||
|
||||
.song-name {
|
||||
font-size: 1.5em;
|
||||
|
||||
min-width: 1em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
align-self: center;
|
||||
color: #999999;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.song-description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container-timeline {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
margin-bottom: .5em;
|
||||
|
||||
.timestamps {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
color: #999;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
$timeline_height: .6em;
|
||||
.timeline {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1em;
|
||||
height: $timeline_height;
|
||||
cursor: pointer;
|
||||
background-color: #242527;
|
||||
border-radius: 0.2em;
|
||||
overflow: visible;
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
border-radius: .2em;
|
||||
}
|
||||
|
||||
.indicator-buffered {
|
||||
background-color: #2f3133;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.indicator-playtime {
|
||||
background-color: #4370a2;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
$thumb_width: 1.2em;
|
||||
$thumb_inner_width: 0.4em;
|
||||
.thumb {
|
||||
position: absolute;
|
||||
|
||||
height: $thumb_width;
|
||||
width: $thumb_width;
|
||||
|
||||
left: -($thumb_width / 2);
|
||||
bottom: -$thumb_width / 2 + $timeline_height / 2;
|
||||
|
||||
background-color: #a5a5a5;
|
||||
box-shadow: 0 0 0.5em 1px #a5a5a53d;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.dot {
|
||||
align-self: center;
|
||||
|
||||
height: $thumb_inner_width;
|
||||
width: $thumb_inner_width;
|
||||
|
||||
|
||||
background-color: #4370a2;
|
||||
box-shadow: 0 0 0.1em 1px hsla(212, 41%, 60%, 1);
|
||||
|
||||
border-radius: 50%;
|
||||
}
|
||||
border-radius: 50%;
|
||||
|
||||
//@include transition(.4s);
|
||||
|
||||
margin-left: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
.button {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
margin-right: .5em;
|
||||
margin-left: .5em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
|
||||
fill: #242527;
|
||||
@include transition($button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #0a0a0a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-play, .button-pause {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-playlist {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: calc(3em + 4px);
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
margin-bottom: 5px;
|
||||
margin-top: 1em;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background: #2b2b28;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 0.2em;
|
||||
border: 1px #161616 solid;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
|
||||
font-size: 1.5em;
|
||||
color: hsla(0, 1%, 40%, 1);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 8em;
|
||||
font-size: .8em;
|
||||
align-self: center;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 3em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: 0.2em;
|
||||
background-color: rgba(43, 43, 40, 1);
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.entry {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
color: #999;
|
||||
border-bottom: 1px solid #242527;
|
||||
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(220, 0%, 20%, 1);
|
||||
}
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
&.animation {
|
||||
@include transition(opacity 0.5s ease-in-out, height 0.5s ease-in);
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
@include transition(opacity 0.5s ease-in-out, height 0.5s ease-in, padding 0.5s ease-in);
|
||||
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&.reordering {
|
||||
z-index: 10000;
|
||||
|
||||
position: fixed;
|
||||
cursor: move;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: 0.2em;
|
||||
background-color: #2b2b28;
|
||||
}
|
||||
|
||||
.container-thumbnail {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
align-self: center;
|
||||
|
||||
height: .9em;
|
||||
width: 1.6em;
|
||||
|
||||
font-size: 3em;
|
||||
position: relative;
|
||||
|
||||
border-radius: 0.05em;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.container-data {
|
||||
margin-left: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
min-width: 2em;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
&.second {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.container-delete {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
margin-left: .5em;
|
||||
|
||||
opacity: .4;
|
||||
@include transition($button_hover_animation_time ease-in-out);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.length {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.current-song {
|
||||
background-color: hsla(130, 50%, 30%, .25);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(130, 50%, 50%, .25);
|
||||
}
|
||||
|
||||
.container-delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reorder-indicator {
|
||||
$indicator_thickness: .2em;
|
||||
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: $indicator_thickness solid hsla(0, 0%, 30%, 1);
|
||||
|
||||
margin-top: $indicator_thickness / -2;
|
||||
margin-bottom: $indicator_thickness / -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-close {
|
||||
font-size: 4em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
opacity: 0.3;
|
||||
|
||||
width: .5em;
|
||||
height: .5em;
|
||||
|
||||
margin-right: .1em;
|
||||
margin-top: .1em;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||
|
||||
&:before, &:after {
|
||||
position: absolute;
|
||||
left: .25em;
|
||||
content: ' ';
|
||||
height: .5em;
|
||||
width: .05em;
|
||||
background-color: #5a5a5a;
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,6 +108,14 @@ button, input, optgroup, select, textarea {
|
|||
line-height: inherit;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-border-radius: 1px!important;
|
||||
|
||||
option {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fieldset {
|
||||
border: unset;
|
||||
|
@ -115,7 +123,6 @@ fieldset {
|
|||
}
|
||||
|
||||
code {
|
||||
background-color: lightgray;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
@ -231,4 +238,9 @@ a.rainbow-letter {
|
|||
font-weight: bold;
|
||||
border-bottom: 1px solid #ab4788;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border-radius: .2em;
|
||||
border: none;
|
||||
}
|
|
@ -27,7 +27,7 @@ $animation_length: .5s;
|
|||
height: 80%; /* "default" settings */
|
||||
width: 100%;
|
||||
|
||||
min-height: 25em;
|
||||
min-height: 27em; /* fits with the music bot interface */
|
||||
min-width: 100px;
|
||||
|
||||
display: flex;
|
||||
|
|
|
@ -67,13 +67,32 @@
|
|||
}
|
||||
|
||||
@mixin chat-scrollbar() {
|
||||
//We've currently no difference
|
||||
@include chat-scrollbar-vertical();
|
||||
//@include chat-scrollbar-horizontal();
|
||||
& {
|
||||
/* for moz */
|
||||
scrollbar-color: #353535 #555;
|
||||
scrollbarWidth: .5em;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
border-radius: .25em;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: .5em;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: .25em;
|
||||
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin chat-scrollbar-vertical() {
|
||||
/*
|
||||
|
||||
& {
|
||||
// for moz
|
||||
scrollbar-color: #353535 #555;
|
||||
|
@ -98,7 +117,7 @@
|
|||
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #555;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
& > .simplebar-track {
|
||||
.simplebar-scrollbar {
|
||||
|
@ -117,7 +136,7 @@
|
|||
}
|
||||
|
||||
@mixin chat-scrollbar-horizontal() {
|
||||
/*
|
||||
|
||||
& {
|
||||
// MOZ
|
||||
scrollbar-color: #353535 #555;
|
||||
|
@ -142,7 +161,7 @@
|
|||
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #555;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@mixin text-dotdotdot() {
|
||||
|
@ -159,3 +178,31 @@
|
|||
@include hex-rgba(background-color, $color);
|
||||
}
|
||||
|
||||
@mixin tooltip($base_with, $icon_size) {
|
||||
.container-tooltip {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
position: relative;
|
||||
width: $base_with;
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
font-size: .9em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img, .icon_em {
|
||||
height: $icon_size;
|
||||
width: $icon_size;
|
||||
|
||||
align-self: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
padding: .75em 24px;
|
||||
|
||||
border-left: 2px solid #0073d4;
|
||||
overflow: hidden;
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
|
@ -91,6 +92,9 @@
|
|||
.container-buttons-connect {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
flex-shrink: 1;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.button-right {
|
||||
|
|
|
@ -0,0 +1,697 @@
|
|||
@import "mixin";
|
||||
@import "properties";
|
||||
|
||||
.modal-body.modal-music-manage {
|
||||
padding: 0 !important;
|
||||
|
||||
display: flex !important;;
|
||||
flex-direction: column !important;;
|
||||
justify-content: stretch !important;;
|
||||
|
||||
width: 80em;
|
||||
min-height: 20em; /* Set it here, so we dont have a inner modal scroll */
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
> .header {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 4em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.category {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 0;
|
||||
width: 50%;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
cursor: pointer;
|
||||
padding-bottom: 2px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
color: #e1e1e1;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
border-bottom: 2px solid #4e4e4e;
|
||||
|
||||
padding-bottom: 0;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
||||
margin-right: -10em;
|
||||
margin-left: -10em;
|
||||
margin-bottom: -.2em;
|
||||
bottom: 0;
|
||||
|
||||
height: 100%;
|
||||
width: calc(100% + 20em);
|
||||
|
||||
box-shadow: inset 0px -1.2em 3em -20px #424242;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: none;
|
||||
border-bottom: 2px solid #0073d4;
|
||||
|
||||
padding-bottom: 0;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
||||
margin-right: -10em;
|
||||
margin-left: -10em;
|
||||
margin-bottom: -.2em;
|
||||
bottom: 0;
|
||||
|
||||
height: 100%;
|
||||
width: calc(100% + 20em);
|
||||
|
||||
box-shadow: inset 0px -1.2em 3em -20px #0073d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 20em;
|
||||
|
||||
background-color: #303036;
|
||||
|
||||
@include tooltip(1.6em, 1em);
|
||||
.container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 20em;
|
||||
height: 40.5em;
|
||||
|
||||
.input-boxed, .btn {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
$border_color: #1e2025;
|
||||
&.category-permissions {
|
||||
.column {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.column-permission {
|
||||
flex-shrink: 1000;
|
||||
flex-grow: 1;
|
||||
min-width: 6em;
|
||||
|
||||
a {
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.master {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slave {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&.column-required {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
a {
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
min-width: 6em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
&.column-client-specific {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
min-width: 20em;
|
||||
width: 30em;
|
||||
}
|
||||
}
|
||||
|
||||
.table-head {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.column {
|
||||
height: 5.5em;
|
||||
padding: .5em;
|
||||
|
||||
justify-content: flex-end!important;
|
||||
|
||||
&.column-permission, &.column-required {
|
||||
color: #e1e1e1;
|
||||
font-weight: bold;
|
||||
|
||||
border-right: 1px solid $border_color;
|
||||
}
|
||||
}
|
||||
|
||||
.select-client {
|
||||
padding-top: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.client-select, .client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: 1em;
|
||||
|
||||
@include text-dotdotdot();
|
||||
|
||||
&.button-search {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
&.button-list-clients, &.button-client-deselect {
|
||||
width: 8em;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 2em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
a {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
|
||||
align-self: center;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.client-info {
|
||||
color: #e1e1e1;
|
||||
font-weight: bold;
|
||||
|
||||
a {
|
||||
flex-shrink: 0;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.htmltag-client {
|
||||
color: #e1e1e1;
|
||||
align-self: center;
|
||||
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-height: 6em;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.entry {
|
||||
height: 2.6em; /* input box + 2 * .5em */
|
||||
padding: .5em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
background-color: unset;
|
||||
|
||||
&:nth-of-type(2n) {
|
||||
background-color: #25252a;
|
||||
}
|
||||
|
||||
border-top: 1px solid $border_color;
|
||||
border-right: 1px solid $border_color;
|
||||
|
||||
.container-input {
|
||||
color: #e1e1e1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: right;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 2em;
|
||||
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
height: 1.6em;
|
||||
|
||||
/* fix the column padding */
|
||||
padding-left: 1em;
|
||||
margin-left: -.5em; /* have a bit of space on both sides */
|
||||
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.container-tooltip {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:focus-within {
|
||||
border-bottom-color: #3f7dbf;
|
||||
|
||||
input {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
@include transition(border-bottom-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
.column-client-specific {
|
||||
position: relative;
|
||||
|
||||
.entry {
|
||||
border-right: unset;
|
||||
}
|
||||
|
||||
.overlay-client-list {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
border-top: 1px solid $border_color;
|
||||
background-color: #303036;
|
||||
padding: .5em;
|
||||
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
|
||||
@include transition(all .25s ease-in-out);
|
||||
|
||||
&.hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container-client-list {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: 0.2em;
|
||||
background-color: #28292b;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
padding-top: 0.25em;
|
||||
padding-bottom: 0.25em;
|
||||
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
background-color: #28292b;
|
||||
color: #676468;
|
||||
|
||||
text-align: center;
|
||||
font-size: 1.3em;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.client {
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
|
||||
.htmltag-client {
|
||||
@include text-dotdotdot();
|
||||
color: #999;
|
||||
font-weight: unset;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2c2d2f;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.category-settings {
|
||||
.container-settings {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 10em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.settings {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
width: 50%;
|
||||
min-width: 15em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
&.settings-bot {
|
||||
border-right: 1px solid $border_color;
|
||||
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.settings-playlist {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
> a {
|
||||
color: #e1e1e1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
> label, > div {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
|
||||
margin-top: .5em;
|
||||
|
||||
* {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
.container-name-country {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.option-bot-name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 6em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.container-country {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 5em;
|
||||
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.country {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.container-replay-mode, .container-max-playlist-size {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
a {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
select, .input-boxed {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 10em;
|
||||
|
||||
margin-left: .5em;
|
||||
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-buttons {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-music-permission-overview {
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
max-height: 8em;
|
||||
|
||||
.container-title {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container-groups {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include chat-scrollbar-horizontal();
|
||||
}
|
||||
|
||||
.container-status {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
@import "mixin";
|
||||
@import "properties";
|
||||
|
||||
.modal-body.modal-newcomer {
|
||||
display: flex!important;
|
||||
flex-direction: column!important;
|
||||
justify-content: stretch!important;
|
||||
|
||||
padding: 0!important;
|
||||
|
||||
min-width: 20em;
|
||||
width: 50em;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
.container-header {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
color: #565656;
|
||||
padding: .5em .5em .25em;
|
||||
|
||||
position: relative;
|
||||
font-size: 1.5em;
|
||||
text-transform: uppercase;
|
||||
|
||||
.step {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
height: 1.25px;
|
||||
//background: linear-gradient(90deg, rgba(49,49,53,1) 80%, rgba(49,49,53,0) 100%);
|
||||
background: rgba(49,49,53,1);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-body {
|
||||
//flex-grow: 1;
|
||||
//flex-shrink: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 18em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
overflow: auto;
|
||||
@include chat-scrollbar-horizontal();
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
padding: .5em .5em .5em;
|
||||
|
||||
&.height-transition {
|
||||
@include transition(max-height .25s ease-in-out, min-height .25s ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step {
|
||||
&.step-welcome, &.step-finish {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.text {
|
||||
align-self: center;
|
||||
h1 {
|
||||
line-height: 1.1em;
|
||||
|
||||
margin-bottom: .8em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.step-welcome h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-height: 15em;
|
||||
max-width: 15em;
|
||||
|
||||
align-self: center;
|
||||
margin-right: 1em;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* for step-identity or step-microphone */
|
||||
.container-settings-identity-profile, .container-settings-audio-microphone {
|
||||
padding: .5em;
|
||||
|
||||
.left .body {
|
||||
// background-color: #19191b;
|
||||
background-color: hsla(220, 4%, 13%, 1);
|
||||
.overlay {
|
||||
background-color: hsla(220, 4%, 13%, 1);
|
||||
|
||||
}
|
||||
.profile.selected {
|
||||
background-color: hsla(240, 2%, 8%, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.step-identity {
|
||||
}
|
||||
|
||||
&.step-microphone {
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
border-top: 1.25px solid rgba(49,49,53,1);
|
||||
padding: .5em;
|
||||
}
|
||||
}
|
|
@ -409,7 +409,7 @@
|
|||
background-image: linear-gradient(0deg, #008aff 2px, rgba(0, 150, 136, 0) 0), linear-gradient(0deg, #393939 1px, transparent 0);
|
||||
|
||||
&:focus {
|
||||
height: calc(2.25em - 1px); /* Center the blue line */
|
||||
height: 2.25em;
|
||||
|
||||
background-size: 100% 100%, 100% 100%;
|
||||
transition-duration: .3s;
|
||||
|
@ -499,6 +499,8 @@
|
|||
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
|
||||
|
||||
@include text-dotdotdot();
|
||||
|
||||
&:hover {
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
@import "mixin";
|
||||
|
||||
.overlay-image-preview {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
|
||||
pointer-events: all;
|
||||
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
opacity: 1;
|
||||
background-color: #000000EF;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.container-menu-bar {
|
||||
position: absolute;
|
||||
|
||||
top: .25em;
|
||||
left: 0;
|
||||
right: .25em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
font-size: 2em;
|
||||
|
||||
margin: .25em;
|
||||
padding: .15em;
|
||||
|
||||
border-radius: .125em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.container-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #FFFFFF1F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-image {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
img {
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 1em;
|
||||
min-width: 1em;
|
||||
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include transition(ease-in-out .25s);
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR"
|
||||
cd "$BASEDIR" || { echo "Failed to enter script base dir"; exit 1; }
|
||||
source ../scripts/resolve_commands.sh
|
||||
|
||||
function generate_declaration() {
|
||||
echo "Generating declarations for project $1 ($2)"
|
||||
|
||||
if [[ -e ${2} ]]; then
|
||||
rm ${2}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to remove old declaration file ($2)!"
|
||||
if [[ -d "${2}" ]]; then
|
||||
rm -r "${2}"; _exit_code=$?
|
||||
if [[ $_exit_code -ne 0 ]]; then
|
||||
echo "Failed to remove old declaration file ($2): $_exit_code!"
|
||||
echo "This could be critical later!"
|
||||
fi
|
||||
fi
|
||||
|
||||
npm run dtsgen -- --config $(pwd)/tsconfig/$1 -v
|
||||
npm run dtsgen -- --config "$(pwd)/tsconfig/$1" -v
|
||||
if [[ ! -e $2 ]]; then
|
||||
echo "Failed to generate definitions"
|
||||
exit 1
|
||||
|
@ -23,12 +23,12 @@ function generate_declaration() {
|
|||
}
|
||||
|
||||
#Generate the loader definitions first
|
||||
app_declaration="declarations/exports_app.d.ts"
|
||||
loader_declaration_app="declarations/exports_loader_app.d.ts"
|
||||
loader_declaration_certaccept="declarations/exports_loader_certaccept.d.ts"
|
||||
app_declaration="../declarations/shared-app/"
|
||||
loader_declaration_app="../declarations/loader/"
|
||||
# loader_declaration_certaccept="declarations/exports_loader_certaccept.d.ts"
|
||||
|
||||
generate_declaration dtsconfig_app.json ${app_declaration}
|
||||
generate_declaration dtsconfig_loader_app.json ${loader_declaration_app}
|
||||
generate_declaration dtsconfig_loader_certaccept.json ${loader_declaration_certaccept}
|
||||
# generate_declaration dtsconfig_loader_certaccept.json ${loader_declaration_certaccept}
|
||||
|
||||
exit 0
|
|
@ -1,56 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd "$BASEDIR"
|
||||
source ../scripts/resolve_commands.sh
|
||||
# The app loader
|
||||
execute_ttsc -p tsconfig/tsconfig_packed_loader_app.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate packed loader file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm run minify-web-rel-file `pwd`/generated/loader_app.min.js `pwd`/generated/loader_app.js
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to minimize packed loader file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The popup certaccept loader
|
||||
execute_ttsc -p tsconfig/tsconfig_packed_loader_certaccept.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate packed loader file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm run minify-web-rel-file `pwd`/generated/loader_certaccept.min.js `pwd`/generated/loader_certaccept.js
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to minimize packed loader file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The main shared source
|
||||
execute_ttsc -p tsconfig/tsconfig_packed.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate packed file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The certaccept source
|
||||
execute_ttsc -p tsconfig/tsconfig_packed_certaccept.json
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to generate packed certaccept file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm run minify-web-rel-file `pwd`/generated/certaccept.min.js `pwd`/generated/certaccept.js
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to minimize the certaccept file!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create packed CSS file
|
||||
./css/generate_packed.sh
|
||||
|
||||
echo "Packed file generated!"
|
||||
exit 0
|
|
@ -145,9 +145,11 @@
|
|||
|
||||
<meta name="app-loader-target" content="app">
|
||||
<div id="scripts">
|
||||
<!--
|
||||
<script type="application/javascript" src="loader/loader_app.min.js" async defer></script>
|
||||
<script type="application/javascript" src="loader/loader_app.js" async defer></script>
|
||||
<script type="application/javascript" src="loader/loader.js?_<?php echo time() ?>" async defer></script>
|
||||
-->
|
||||
<script type="application/javascript" src="js/loader.js?_<?php echo time() ?>" async defer></script>
|
||||
</div>
|
||||
|
||||
<!-- Loading screen -->
|
||||
|
|
|
@ -224,6 +224,26 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="contextMenu" class="context-menu"></div>
|
||||
<div class="overlay-image-preview hidden" id="overlay-image-preview">
|
||||
<div class="container-menu-bar">
|
||||
<div class="entry button-open-in-window">
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_image_preview_browse.svg">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Why would you like to download images?
|
||||
<div class="entry button-download">
|
||||
<div class="icon_em client-download"></div>
|
||||
</div>
|
||||
-->
|
||||
<div class="entry button-close">
|
||||
<div class="icon_em client-close_button"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-image">
|
||||
<img src="https://forum.teaspeak.de/index.php?attachments/1581248374635-png.2098/">
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_chat" type="text/html">
|
||||
|
@ -254,11 +274,26 @@
|
|||
<div class="block left mode-based mode-private_chat">
|
||||
<div class="button button-switch-chat-channel">{{tr "Switch to channel chat" /}}</div>
|
||||
</div>
|
||||
|
||||
<div class="block left mode-based mode-music_bot">
|
||||
<div class="title"> </div>
|
||||
<div class="value button bot-manage">{{tr "Manage Bot" /}}</div>
|
||||
</div>
|
||||
|
||||
<div class="block left mode-based mode-client_info">
|
||||
<div class="spacer-client-info"></div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-based mode-private_chat mode-channel_chat">
|
||||
<div class="block right mode-based mode-private_chat">
|
||||
<div class="title">{{tr "Private chats" /}}
|
||||
<div class="container-indicator">
|
||||
<div class="chat-unread-counter">-1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value button chat-counter">Hmm... Something seems to be wrong!</div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-basedt mode-channel_chat">
|
||||
<div class="title">{{tr "Private chats" /}}
|
||||
<div class="container-indicator">
|
||||
<div class="chat-unread-counter">-1</div>
|
||||
|
@ -271,6 +306,11 @@
|
|||
<div class="title"> </div>
|
||||
<div class="value button open-conversation">error: open conversation</div>
|
||||
</div>
|
||||
|
||||
<div class="block right mode-based mode-music_bot">
|
||||
<div class="title"> </div>
|
||||
<div class="value button bot-add-song">{{tr "Add song" /}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
@ -425,42 +465,55 @@
|
|||
<div class="general-info">
|
||||
<div class="block block-left">
|
||||
<div class="container-property">
|
||||
<div class="icon_em client-home"></div>
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_online_time.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Online since" /}}</div>
|
||||
<div class="value client-online-time">error: online-time</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<div class="icon_em client-home"></div>
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_country.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Country" /}}</div>
|
||||
<div class="value client-country"><a>error: country</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property container-teaforo">
|
||||
<div class="icon_em client-home"></div>
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_forum_account.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "TeaSpeak Forum Account" /}}</div>
|
||||
<div class="value client-teaforo-account"><a>error: online-time</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<div class="icon_em client-home"></div>
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_version.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Version" /}}</div>
|
||||
<div class="value client-version"><a>error: version</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property">
|
||||
<div class="icon_em client-home"></div>
|
||||
<!-- Using inline style because the icon itself is a bit off. A better way would be to edit the icon itself, but I'm unable to do so.... -->
|
||||
<div class="container-icon" style="margin-right: .16em; margin-left: .04em;">
|
||||
<img src="img/icon_client_volume.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Volume" /}}</div>
|
||||
<div class="value client-local-volume">error: local volume</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-property list container-client-status">
|
||||
<div class="icon_em client-home"></div>
|
||||
<div class="container-icon">
|
||||
<img src="img/icon_client_status.svg" />
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="title">{{tr "Status" /}}</div>
|
||||
<div class="value client-status">
|
||||
|
@ -494,6 +547,166 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_chat_music_info" type="text/html">
|
||||
<div class="container-music-info">
|
||||
<div class="player">
|
||||
<div class="container-thumbnail">
|
||||
<div class="thumbnail">
|
||||
<!-- https://i.ytimg.com/vi/DeXoACwOT1o/maxresdefault.jpg -->
|
||||
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
|
||||
<img src="https://i.ytimg.com/vi/DeXoACwOT1o/maxresdefault.jpg">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-song-info">
|
||||
<a class="song-name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</a>
|
||||
<a class="song-description"></a>
|
||||
</div>
|
||||
<div class="container-timeline">
|
||||
<div class="timestamps">
|
||||
<div class="current">00:01:13</div>
|
||||
<div class="max">00:03:22</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="indicator indicator-buffered"></div>
|
||||
<div class="indicator indicator-playtime"></div>
|
||||
<div class="thumb"><div class="dot"></div></div>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<div class="button button-rewind">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path transform="rotate(180, 256, 256)" d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
|
||||
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
|
||||
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
|
||||
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
|
||||
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-play">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path d="M500.203,236.907L30.869,2.24c-6.613-3.285-14.443-2.944-20.736,0.939C3.84,7.083,0,13.931,0,21.333v469.333
|
||||
c0,7.403,3.84,14.251,10.133,18.155c3.413,2.112,7.296,3.179,11.2,3.179c3.264,0,6.528-0.747,9.536-2.24l469.333-234.667
|
||||
C507.435,271.467,512,264.085,512,256S507.435,240.533,500.203,236.907z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-pause">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path transform='rotate(90, 256, 256)' d="M85.333,213.333h341.333C473.728,213.333,512,175.061,512,128s-38.272-85.333-85.333-85.333H85.333
|
||||
C38.272,42.667,0,80.939,0,128S38.272,213.333,85.333,213.333z"/>
|
||||
<path transform='rotate(90, 256, 256)' d="M426.667,298.667H85.333C38.272,298.667,0,336.939,0,384s38.272,85.333,85.333,85.333h341.333
|
||||
C473.728,469.333,512,431.061,512,384S473.728,298.667,426.667,298.667z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button button-forward">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path d="M504.171,239.489l-234.667-192c-6.357-5.227-15.189-6.293-22.656-2.773c-7.424,3.541-12.181,11.051-12.181,19.285v146.987
|
||||
L34.837,47.489c-6.379-5.227-15.189-6.293-22.656-2.773C4.757,48.257,0,55.767,0,64.001v384c0,8.235,4.757,15.744,12.181,19.285
|
||||
c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l199.829-163.499v146.987
|
||||
c0,8.235,4.757,15.744,12.181,19.285c2.923,1.365,6.059,2.048,9.152,2.048c4.843,0,9.621-1.643,13.504-4.821l234.667-192
|
||||
c4.949-4.053,7.829-10.112,7.829-16.512S509.12,243.543,504.171,239.489z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-playlist">
|
||||
<div class="overlay overlay-loading">
|
||||
<a>{{tr "Fetching playlist..." /}}</a>
|
||||
</div>
|
||||
<div class="overlay overlay-no-permissions">
|
||||
<a>{{tr "You don't have permissions to see this playlist" /}}</a>
|
||||
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
<div class="overlay overlay-error">
|
||||
<a>{{tr "An error occurred while fetching the playlist" /}}</a>
|
||||
<button class="btn btn-blue button-reload-playlist">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
<div class="overlay overlay-empty">
|
||||
<a>{{tr "The playlist is currently empty." /}}</a>
|
||||
<button class="btn btn-green button-song-add">{{tr "Add a song" /}}</button>
|
||||
</div>
|
||||
<div class="playlist">
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<!-- <img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%"> -->
|
||||
<img src="https://i.ytimg.com/vi/KaXXVzGy7Y8/maxresdefault.jpg">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">2-Hours Epic Music | THE POWER OF EPIC MUSIC - Best Of Collection - Vol.5 - 2019</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">It is time for another 2-Hour Epic Music Mix. So far 2019 has been one amazing year for the orchestral epic music genre and i cannot wait for more. Also incl...</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">This is an example song description which needs some work</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entry">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png" style="height: 100%; width: 100%">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">CHOSEN ONE | BEST EPIC MUSIC OF 2018 (Part 4) And some more info</div>
|
||||
<div class="container-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">This is an example song description which needs some work</div>
|
||||
<div class="length">00:22:01</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-close"></div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_frame_music_playlist_entry" type="text/html">
|
||||
<div class="entry shown">
|
||||
<div class="container-thumbnail">
|
||||
<img src="img/music/no-thumbnail.png">
|
||||
</div>
|
||||
<div class="container-data">
|
||||
<div class="row">
|
||||
<div class="name">error: name</div>
|
||||
<div class="container-delete button-delete"><img src="img/icon_conversation_message_delete.svg" alt="X"></div>
|
||||
</div>
|
||||
|
||||
<div class="row second">
|
||||
<div class="description">error: description</div>
|
||||
<div class="length">--:--:--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_select_info" type="text/html">
|
||||
<div class="select_info" style="width: 100%; max-width: 100%">
|
||||
<button type="button" class="close button-modal-close" aria-label="Close">
|
||||
|
@ -2024,102 +2237,7 @@
|
|||
</div>
|
||||
|
||||
<div class="container audio-microphone">
|
||||
<div class="left">
|
||||
<div class="header">
|
||||
<a>{{tr "Select your Microphone Device" /}}</a>
|
||||
<button class="btn btn-info button-update">{{tr "Update" /}}</button>
|
||||
</div>
|
||||
<div class="body container-devices">
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="header">
|
||||
<a>{{tr "Microphone Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-volume">
|
||||
<a>{{tr "Volume" /}}</a>
|
||||
<div class="container-slider">
|
||||
<div class="filler" style="width: 30%"></div>
|
||||
<div class="thumb container-tooltip" style="left: 30%">
|
||||
<div class="tooltip">
|
||||
<a>86%</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-select-vad">
|
||||
<div class="fieldset">
|
||||
<div class="container container-ppt">
|
||||
<label class="container-ppt">
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="push_to_talk">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Push to Talk" /}}</a>
|
||||
</label>
|
||||
<div class="container-button">
|
||||
<button class="btn">{{tr "T" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container container-vad">
|
||||
<label>
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="threshold">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Voice activity detection" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
<div class="container container-always-active">
|
||||
<label>
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="active">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Always active" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<a>{{tr "Sensitivity Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-sensitivity">
|
||||
<div class="container-activity-bar">
|
||||
<div class="bar-hider" style="width: 80%;"></div>
|
||||
<div class="bar-error"></div>
|
||||
<div class="thumb container-tooltip" style="left: 20%">
|
||||
<div class="tooltip">
|
||||
<a>20%</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<a>{{tr "Advanced Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-advanced">
|
||||
<div class="container-ppt-delay">
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="delay-enabled">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Delay on Push to Talk" /}}</a>
|
||||
</label>
|
||||
<div class="container-input">
|
||||
<input type="number" class="delay-time" min="0" max="4" step="0.1">
|
||||
<label>{{tr "Sec" /}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{include tmpl="tmpl_settings-microphone" /}}
|
||||
</div>
|
||||
<div class="container audio-speaker">
|
||||
<div class="left">
|
||||
|
@ -2208,103 +2326,7 @@
|
|||
</div>
|
||||
|
||||
<div class="container identity-profiles">
|
||||
<div class="left">
|
||||
<div class="header">
|
||||
<a>{{tr "Your Profiles" /}}</a>
|
||||
<button class="btn btn-info button-create">{{tr "Create new" /}}</button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-profiles">
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-danger button-delete">{{tr "Delete selected" /}}</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-success button-set-default">{{tr "Select as Default" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="header">
|
||||
<a>{{tr "Profile Settings" /}}</a>
|
||||
<div class="spacer"></div>
|
||||
<div class="container-avatar"></div>
|
||||
<button class="btn btn-info button-change-avatar">{{tr "Change Avatar" /}}</button>
|
||||
<!-- AVATAR -->
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-general">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Profile Name" /}}</label>
|
||||
<input class="form-control profile-name">
|
||||
<div class="invalid-feedback">{{tr "Profile name is invalid" /}}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{tr "Default Nickname" /}}</label>
|
||||
<input class="form-control profile-default-name"
|
||||
placeholder="Another TeaSpeak user">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{tr "Identity Type" /}}</label>
|
||||
<select class="form-control profile-identity-type">
|
||||
<option value="unset" style="display: none">{{tr "Unset" /}}</option>
|
||||
<option value="teaforo">{{tr "Forum Account" /}}</option>
|
||||
<option value="teamspeak">{{tr "TeamSpeak Identity" /}}</option>
|
||||
<option value="nickname">{{tr "Nickname (Debug only!)" /}}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">{{tr "Invalid identity type" /}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-teamspeak">
|
||||
<div class="container-valid">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Unique-ID" /}}</label>
|
||||
<input class="form-control unique-id" readonly>
|
||||
</div>
|
||||
<div class="container-level">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Level" /}}</label>
|
||||
<input class="form-control current-level" readonly>
|
||||
</div>
|
||||
<button class="btn button-improve">{{tr "Improve" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-invalid">
|
||||
{{tr "You have'nt generated/imported an identity." /}}<br>
|
||||
{{tr "Generate a new one or import one." /}}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-danger button-new">{{tr "Generate new" /}}</button>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-danger button-import">{{tr "Import identity" /}}
|
||||
</button>
|
||||
<button class="btn btn-success button-export">{{tr "Export identity" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-teaforo">
|
||||
<div class="container-valid">
|
||||
{{tr "You're using your forum account as identification" /}}
|
||||
</div>
|
||||
<div class="container-invalid">
|
||||
<a>{{tr "You cant use your TeaSpeak forum account. You're not connected with your forum Account!" /}}</a>
|
||||
<button class="btn btn-success button-setup">{{tr "Setup your connection" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-nickname">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Nickname" /}}</label>
|
||||
<input class="form-control nickname">
|
||||
<div class="invalid-feedback">{{tr "Invalid nickname. Name must be at least 5 characters" /}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{include tmpl="tmpl_settings-profiles" /}}
|
||||
</div>
|
||||
<div class="container identity-forum">
|
||||
<div class="fill">
|
||||
|
@ -2345,6 +2367,238 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script class="jsrender-template" id="tmpl_settings-microphone" type="text/html">
|
||||
<div class="container-settings-audio-microphone">
|
||||
<div class="left highlightable highlight-microphone-list">
|
||||
<div class="header">
|
||||
<a>{{tr "Select your Microphone Device" /}}</a>
|
||||
<button class="btn btn-info button-update">{{tr "Update" /}}</button>
|
||||
</div>
|
||||
<div class="body container-devices">
|
||||
<div class="overlay overlay-error">
|
||||
<a class="error-text"></a>
|
||||
</div>
|
||||
<div class="overlay overlay-loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right highlightable highlight-microphone-settings">
|
||||
<div class="header">
|
||||
<a>{{tr "Microphone Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-volume">
|
||||
<a>{{tr "Volume" /}}</a>
|
||||
<div class="container-slider">
|
||||
<div class="filler" style="width: 30%"></div>
|
||||
<div class="thumb container-tooltip" style="left: 30%">
|
||||
<div class="tooltip">
|
||||
<a>86%</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-select-vad">
|
||||
<div class="fieldset">
|
||||
<div class="container container-ppt">
|
||||
<label class="container-ppt">
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="push_to_talk">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Push to Talk" /}}</a>
|
||||
</label>
|
||||
<div class="container-button">
|
||||
<button class="btn">{{tr "T" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container container-vad">
|
||||
<label>
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="threshold">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Voice activity detection" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
<div class="container container-always-active">
|
||||
<label>
|
||||
<div class="ratio-button">
|
||||
<input type="radio" name="vad-type" value="active">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Always active" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<a>{{tr "Sensitivity Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-sensitivity">
|
||||
<div class="container-activity-bar">
|
||||
<div class="bar-hider" style="width: 80%;"></div>
|
||||
<div class="bar-error"></div>
|
||||
<div class="thumb container-tooltip" style="left: 20%">
|
||||
<div class="tooltip">
|
||||
<a>20%</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<a>{{tr "Advanced Settings" /}}</a>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-advanced">
|
||||
<div class="container-ppt-delay">
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="delay-enabled">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Delay on Push to Talk" /}}</a>
|
||||
</label>
|
||||
<div class="container-input">
|
||||
<input type="number" class="delay-time" min="0" max="4" step="0.1">
|
||||
<label>{{tr "Sec" /}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-background"></div>
|
||||
<div class="container-help-text window-resize-listener">
|
||||
<a class="help-text">Hello nice to see you. This is even working with new lines etc....</a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_settings-profiles" type="text/html">
|
||||
<div class="container-settings-identity-profile">
|
||||
<div class="left highlight-profile-list highlightable">
|
||||
<div class="header">
|
||||
<a>{{tr "Your Profiles" /}}</a>
|
||||
<button class="btn btn-info button-create">{{tr "Create new" /}}</button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-profiles">
|
||||
<div class="overlay overlay-error"><a class="error">error: error</a></div>
|
||||
<div class="overlay overlay-timeout">
|
||||
<a>{{tr "Timeout while loading" /}}</a><br>
|
||||
<button class="btn btn-blue button-reload-list">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
<div class="overlay overlay-empty">
|
||||
<a>{{tr "You've not profiles yet" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-danger button-delete">{{tr "Delete selected" /}}</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-success button-set-default">{{tr "Select as Default" /}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-small">
|
||||
<button class="btn btn-danger button-delete">{{tr "Delete" /}}</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-success button-set-default">{{tr "Default" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="header">
|
||||
<a>{{tr "Profile Settings" /}}</a>
|
||||
<div class="spacer"></div>
|
||||
<div class="container-avatar"></div>
|
||||
<button class="btn btn-info button-change-avatar">{{tr "Change Avatar" /}}</button>
|
||||
<!-- AVATAR -->
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container-general highlight-profile-settings highlightable">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Profile Name" /}}</label>
|
||||
<input class="form-control profile-name">
|
||||
<div class="invalid-feedback">{{tr "Profile name is invalid" /}}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{tr "Default Nickname" /}}</label>
|
||||
<input class="form-control profile-default-name"
|
||||
placeholder="Another TeaSpeak user">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{tr "Identity Type" /}}</label>
|
||||
<select class="form-control profile-identity-type">
|
||||
<option value="error" style="display: none">error: error</option>
|
||||
<option value="unset" style="display: none">{{tr "Unset" /}}</option>
|
||||
<option value="teaforo">{{tr "Forum Account" /}}</option>
|
||||
<option value="teamspeak">{{tr "TeamSpeak Identity" /}}</option>
|
||||
<option value="nickname">{{tr "Nickname (Debug only!)" /}}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">{{tr "Invalid identity type" /}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-teamspeak">
|
||||
<div class="container-valid">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Unique-ID" /}}</label>
|
||||
<input class="form-control unique-id" readonly>
|
||||
</div>
|
||||
<div class="container-level">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Level" /}}</label>
|
||||
<input class="form-control current-level" readonly>
|
||||
</div>
|
||||
<button class="btn button-improve">{{tr "Improve" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-invalid">
|
||||
{{tr "You have'nt generated/imported an identity." /}}<br>
|
||||
{{tr "Generate a new one or import one." /}}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-danger button-new">{{tr "Generate new" /}}</button>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-danger button-import">{{tr "Import identity" /}}
|
||||
</button>
|
||||
<button class="btn btn-success button-export">{{tr "Export identity" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-teaforo">
|
||||
<div class="container-valid">
|
||||
{{tr "You're using your forum account as identification" /}}
|
||||
</div>
|
||||
<div class="container-invalid">
|
||||
<a>{{tr "You cant use your TeaSpeak forum account. You're not connected with your forum Account!" /}}</a>
|
||||
<button class="btn btn-success button-setup">{{tr "Setup your connection" /}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-nickname">
|
||||
<div class="form-group">
|
||||
<label>{{tr "Nickname" /}}</label>
|
||||
<input class="form-control nickname">
|
||||
<div class="invalid-feedback">{{tr "Invalid nickname. Name must be at least 5 characters" /}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="container-highlight-dummy highlight-identity-settings highlightable"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-background"></div>
|
||||
<div class="container-help-text window-resize-listener">
|
||||
<a class="help-text">Hello nice to see you. This is even working with new lines etc....</a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_settings-sound_entry" type="text/html">
|
||||
<div class="entry">
|
||||
<div class="column sound-name">
|
||||
|
@ -2482,13 +2736,13 @@
|
|||
</div>
|
||||
{{else type == "default" }}
|
||||
<div class="entry default {{if selected}}selected{{/if}}">
|
||||
<div class="country flag-gb" title="{{*:i18n.country_name('gb')}}"></div>
|
||||
<div class="country flag-gb" title="{{>fallback_country_name}}"></div>
|
||||
<div class="name">{{tr "English (Default / Fallback)" /}}</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="entry translation {{if selected}}selected{{/if}}" parent-repository="{{:id}}">
|
||||
<div class="country flag-{{:country_code}}"
|
||||
title="{{*:i18n.country_name(data.country_code || 'XX')}}"></div>
|
||||
title="{{>country_name}}"></div>
|
||||
<div class="name">{{> name}}</div>
|
||||
<div class="button button-info">
|
||||
<div class="icon client-about"></div>
|
||||
|
@ -2647,12 +2901,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_newcomer" type="text/html">
|
||||
<div> <!-- required for the renderer -->
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Permission overview -->
|
||||
<script class="jsrender-template" id="tmpl_server_permissions" type="text/html">
|
||||
<div class="container">
|
||||
|
|
|
@ -0,0 +1,574 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Template Music manage</title>
|
||||
</head>
|
||||
<body>
|
||||
<script class="jsrender-template" id="tmpl_music_manage" type="text/html">
|
||||
<div>
|
||||
<div class="header">
|
||||
<div class="category category-settings">
|
||||
<a>{{tr "Settings" /}}</a>
|
||||
</div>
|
||||
<div class="category category-permissions">
|
||||
<a>{{tr "Permissions" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="container category-settings">
|
||||
<div class="container-settings">
|
||||
<div class="settings settings-bot">
|
||||
<a>{{tr "Music Bot" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-name-country">
|
||||
<input class="input-boxed option-bot-name" placeholder="Bot name" max="30">
|
||||
<div class="input-boxed container-country">
|
||||
<input class="option-bot-country" maxlength="2">
|
||||
<div class="country flag-de"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="option-channel-commander">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Music bot is channel commander" /}}</a>
|
||||
</label>
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="option-priority-speaker">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Music bot is priority speaker" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings settings-playlist">
|
||||
<a>{{tr "Replay" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-replay-mode">
|
||||
<a>{{tr "Replay mode" /}}</a>
|
||||
<select class="input-boxed option-replay-mode">
|
||||
<option value="-1" style="display: none;">{{tr "Unknown" /}}</option>
|
||||
<option value="0">{{tr "Normal" /}}</option>
|
||||
<option value="1">{{tr "Looped" /}}</option>
|
||||
<option value="2">{{tr "Single-Looped" /}}</option>
|
||||
<option value="3">{{tr "Shuffle" /}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="container-max-playlist-size">
|
||||
<a>{{tr "Max playlist size" /}}</a>
|
||||
<div class="input-boxed">
|
||||
<input type="number" min="0" value="60" maxlength="6">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a>You could set this value up to X.</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="option-delete-played-songs">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Delete played songs from playlist" /}}</a>
|
||||
</label>
|
||||
<label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="option-notify-songs-change">
|
||||
<div class="mark"></div>
|
||||
</div>
|
||||
<a>{{tr "Send a channel message when the song changes" /}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-buttons">
|
||||
<button class="btn btn-blue button-reload">{{tr "Reload" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container category-permissions">
|
||||
<div class="table-head">
|
||||
<div class="column column-permission">
|
||||
<a>{{tr "Permission" /}}</a>
|
||||
</div>
|
||||
<div class="column column-required">
|
||||
<a>{{tr "Required value" /}}</a>
|
||||
</div>
|
||||
<div class="column column-client-specific">
|
||||
<div class="client-select">
|
||||
<div class="row">
|
||||
<a>{{tr "Search for an client:" /}}</a>
|
||||
<button class="btn btn-blue button-list-clients">error: client list</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" class="input-boxed input-search" placeholder="error: placeholder" maxlength="40">
|
||||
<button class="btn btn-green button-search">{{tr "Search" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="client-info hidden">
|
||||
<div class="row">
|
||||
<a></a>
|
||||
<button class="btn btn-blue button-client-deselect">{{tr "Unselect" /}}</button>
|
||||
</div>
|
||||
<div class="row container-selected-client">
|
||||
<a>{{tr "Showing permissions for:" /}}</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div class="column column-permission">
|
||||
<div class="entry master">
|
||||
<a>{{tr "Bot permissions" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to rename the music bot" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to modify the music bot permissions" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to delete the music bot" /}}</a>
|
||||
</div>
|
||||
<div class="entry master">
|
||||
<a>{{tr "Playlist permissions" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to see the playlist songs" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power for editing playlist settings" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power for viewing playlist permissions" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power for editing playlist permissions" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power for adding a song to the playlist" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to reorder a song within the playlist" /}}</a>
|
||||
</div>
|
||||
<div class="entry slave">
|
||||
<a>{{tr "Power to delete a song from the playlist" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column column-required">
|
||||
<div class="entry"></div> <!-- bot permissions -->
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_client_music_needed_rename_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could rename the bot:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_client_music_needed_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could modify the bot:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}<br>
|
||||
b_virtualserver_permission_find
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_client_music_needed_delete_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could delete the bot:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry"></div> <!-- play permissions -->
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_needed_view_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could view the playlist:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_needed_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could edit the playlist settings:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="b_virtualserver_playlist_permission_list">
|
||||
<div style="flex-grow: 1; flex-shrink: 1; min-width: 0"></div>
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could view the playlist permissions:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_needed_permission_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could edit the playlist permissions:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_song_needed_add_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could add a song:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_song_needed_move_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could reorder the songs:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input general-permission" x-permission="i_playlist_song_needed_remove_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip_music_permission.svg">
|
||||
<div class="tooltip">
|
||||
<a class="tooltip-music-permission-overview">
|
||||
<div class="container-title">{{tr "These groups could delete a song:" /}}</div>
|
||||
<div class="container-status status-no-groups">
|
||||
{{tr "No group could do that." /}}
|
||||
</div>
|
||||
<div class="container-status status-loading">
|
||||
{{tr "loading..." /}}
|
||||
</div>
|
||||
<div class="container-status status-error-permission">
|
||||
{{tr "failed on permission" /}}
|
||||
</div>
|
||||
<div class="container-status status-error">
|
||||
error: error
|
||||
</div>
|
||||
<div class="container-groups"> </div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column column-client-specific">
|
||||
<div class="entry"></div> <!-- bot permissions -->
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_client_music_rename_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-apply"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_client_music_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_client_music_delete_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry"></div> <!-- play permissions -->
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_view_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="b_virtualserver_playlist_permission_list">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could no see the permissions" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_permission_modify_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_song_add_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_song_move_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<div class="container-input client-permission" x-permission="i_playlist_song_remove_power">
|
||||
<input type="number" min="0" value="60">
|
||||
<div class="container-tooltip">
|
||||
<div class="icon_em client-delete"></div>
|
||||
<div class="tooltip">
|
||||
<a>{{tr "Client could perform this action" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-client-list">
|
||||
<div class="title">{{tr "Clients which have special permissions:" /}}</div>
|
||||
<div class="container-client-list">
|
||||
<div class="overlay overlay-filter-no-result">
|
||||
<a>{{tr "No clients match the filter" /}}</a>
|
||||
</div>
|
||||
<div class="overlay overlay-empty-list">
|
||||
<a>{{tr "No clients available" /}}</a>
|
||||
</div>
|
||||
<div class="overlay overlay-query-error">
|
||||
<a>error: error text</a>
|
||||
</div>
|
||||
<div class="overlay overlay-query-error-permissions">
|
||||
<a>error: permission error text</a>
|
||||
</div>
|
||||
<div class="overlay overlay-refreshing">
|
||||
<a>{{tr "Loading..." /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-buttons">
|
||||
<button class="btn btn-blue button-clientlist-refresh">{{tr "Refresh client list" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-buttons">
|
||||
<button class="btn btn-blue button-permission-refresh">{{tr "Refresh" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Template modal newcomer</title>
|
||||
</head>
|
||||
<body>
|
||||
<script class="jsrender-template" id="tmpl_newcomer" type="text/html">
|
||||
<div> <!-- required for the renderer -->
|
||||
<div class="container-header">
|
||||
<!-- We don't have a title here because we've put it into the body. -->
|
||||
<!-- <div class="step step-welcome">{{tr "Welcome to the easy setup guide!" /}} </div> -->
|
||||
<div class="step step-identity">{{tr "Identity setup" /}}</div>
|
||||
<div class="step step-microphone">{{tr "Microphone setup" /}}</div>
|
||||
<div class="step step-speaker">{{tr "Speaker setup" /}}</div>
|
||||
<!-- <div class="step step-finish">{{tr "Setup finished" /}}</div> -->
|
||||
</div>
|
||||
<div class="container-body">
|
||||
<div class="body">
|
||||
<div class="step step-welcome">
|
||||
<div class="logo">
|
||||
<img src="img/teaspeak_cup_animated.png">
|
||||
</div>
|
||||
<div class="text">
|
||||
<h1>{{tr "Welcome dear TeaSpeak user." /}}</h1>
|
||||
{{tr "We would like to setup a few things before you're ready to go.<br>" /}}
|
||||
{{tr "Dont worry! We'll guide you thru the basic setup process." /}}<br>
|
||||
{{tr "Together we'll go thru these steps:" /}}
|
||||
<ol>
|
||||
<li>{{tr "Welcome Greeting" /}}</li>
|
||||
<li>{{tr "Microphone configuration" /}}</li>
|
||||
<li>{{tr "Identity setup" /}}</li>
|
||||
{{if !is_web}}
|
||||
<!-- <li>{{tr "Speaker configuration" /}}</li> -->
|
||||
{{/if}}
|
||||
</ol>
|
||||
{{tr "It is save to exit this guide at any point and directly jump ahead using the client." /}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="step step-microphone">
|
||||
{{include tmpl="tmpl_settings-microphone" /}}
|
||||
</div>
|
||||
<div class="step step-identity">
|
||||
{{include tmpl="tmpl_settings-profiles" /}}
|
||||
</div>
|
||||
<div class="step step-speaker">
|
||||
<h1>TODO</h1>
|
||||
</div>
|
||||
<div class="step step-finish">
|
||||
<div class="logo">
|
||||
<img src="img/teaspeak_cup_animated.png">
|
||||
</div>
|
||||
<div class="text">
|
||||
<h1>{{tr "Congratulations,<br>your done setting up TeaSpeak" /}}</h1>
|
||||
{{tr "You're done setting up your client. But dont worry,<br>" /}}
|
||||
{{tr "your could find all these settings within the settings menu<br>" /}}
|
||||
{{tr 'To open the client settings click on "tools" and than "settings"<br>' /}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-red button-last-step">error: last step</button>
|
||||
<button class="btn btn-green button-next-step">error: next step</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -33,14 +33,14 @@
|
|||
{
|
||||
"key": "tr_gt",
|
||||
"country_code": "tr",
|
||||
"path": "tr_google_translate.translation",
|
||||
"path": "tr_translate.translation",
|
||||
|
||||
"name": "Turkey translation, based on Google Translate",
|
||||
"name": "Türkçe / Turkish",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||
"email": "gtr.i18n.client@teaspeak.de"
|
||||
}
|
||||
{
|
||||
"name": "Erdem Bekci",
|
||||
"email": "bekcierdem@gmail.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
"name": "Русский",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||
"email": "gtr.i18n.client@teaspeak.de"
|
||||
"name": "Администрация хостинга 1GTS.RU",
|
||||
"email": "support@1gts.ru"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -9072,6 +9072,15 @@
|
|||
"message": "Connect to a server in another tab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Подключиться в той же вкладке",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Connect in same tab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Отключиться от текущего сервера",
|
||||
"flags": [
|
||||
|
@ -9621,6 +9630,24 @@
|
|||
"message": "Current language:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Фиксированная метка времени чата (Убирает такие метки как \"2 секунды назад\"... и т.д.)",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Fixed chat timestamp (Disables \"2 seconds ago\"... etc)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Отметка времени в чате (Добавляет такие метки как \"Вчера в...\" или \"Сегодня в...\")",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Colloquial chat timestamp (Enables \"Yesterday at ...\" or \"Today at ...\")"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Переключить чат на выбранный канал",
|
||||
"flags": [
|
||||
|
@ -9649,7 +9676,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"translated": "Включает поддержку ввода markdown для чата.",
|
||||
"translated": "Включить для чата поддержку синтаксиса Markdown",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
|
@ -9657,6 +9684,15 @@
|
|||
"message": "Enables markdown input support for chat. "
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Включить для чата поддержку синтаксиса BBCode.",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enable the BBCode syntax for your chat."
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Размер смайликов в чате:",
|
||||
"flags": [
|
||||
|
@ -11087,8 +11123,908 @@
|
|||
"key": {
|
||||
"message": "Add Bookmark"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Это недавно созданный сервер, для которого еще не были запрошены права администратора.",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "This is a newly created server for which administrator privileges have not yet been claimed."
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Для получения прав администратора, пожалуйста введите \"ключ привилегий\", который был автоматически сгенерирован при создании этого сервера.",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"translated": "Использовать ключ привилегий ",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Use a privilege key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите ключ привилегий",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter Privilege Key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Имя канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Channel name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Загрузка Аватара",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Avatar Upload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Выберите аватар",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Select avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Загрузить аватар",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Upload avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Удалить аватар",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Delete avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Поддерживаемые типы изображений: SVG, PNG, JPEG",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Supported image types: SVG, PNG, JPEG"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Поддерживаемые размеры изображения: любые",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Supported image sizes: any"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Предосмотр",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Preview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Информация о клиенте",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Client info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Аватар в чате",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Chat avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Аватар в списке чатов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Chat list avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Аватар успешно загружен",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Avatar successfully uploaded"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Ваш аватар был успешно загружен!",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Your avatar has been uploaded successfully!"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Управление ботом",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Manage Bot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите URL песни",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter song URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите URL песни или потока",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter song URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Пожалуйста, введите URL песни или потока",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Please enter the target song URL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Воспроизвести тестовый звук",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Play test Sound"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Информация о сервере: ",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Server Information: "
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Пропускная способность",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Show Bandwidth"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Песня не выбрана",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "No song selected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Управление Плейлистом",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Playlist Manage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Музыкальный Бот",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Music Bot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Воспроизведение",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Replay"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Музыкальный бот как командир канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Music bot is channel commander"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Музыкальный бот как приоритетный собеседник",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Music bot is priority speaker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Удалять песни из списка воспроизведения",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Delete played songs from playlist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Повторять плэйлист",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Looped"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Повторять трек",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Single-Looped"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Максимальный размер плейлиста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Max playlist size"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Отправлять сообщение в канал, при смене трека",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Send a channel message when the song changes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Вы можете установить это значение до X.",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "You could set this value up to X."
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Разрешение",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Permission"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Значение",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Required value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Поиск клиента:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Search for an client:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Uid клиента или ID базы данных",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Client uid or database id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Поиск",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Список клиентов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "List clients"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Скрыть клиентов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Hide clients"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Поиск списка клиентов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Search client list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Клиенты, имеющие специальные разрешения:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Clients which have special permissions:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Нет доступных клиентов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "No clients available"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Обновить список клиентов",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Refresh client list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Показаны разрешения для:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Showing permissions for:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Отменить выбор",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Unselect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Разрешения бота",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Bot permissions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность переименовать музыкальный бот",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to rename the music bot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут переименовать бота:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could rename the bot:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность изменять разрешения музыкального бота",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to modify the music bot permissions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут менять настройки бота:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could modify the bot:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность удаления музыкального бота",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to delete the music bot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут удалить бота:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could delete the bot:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Разрешения плейлиста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Playlist permissions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность просмотра списка воспроизведения песен",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to see the playlist songs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут просматривать список воспроизведения:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could view the playlist:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность редактирования параметров плейлиста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power for editing playlist settings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут редактировать параметры плейлиста:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could edit the playlist settings:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность просмотра разрешений плейлиста:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power for viewing playlist permissions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут просматривать разрешения плейлиста:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could view the playlist permissions:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность редактирования разрешений плейлиста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power for editing playlist permissions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут редактировать разрешения плэйлиста:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could edit the playlist permissions:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность добавления песни в плейлист",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power for adding a song to the playlist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут добавить песню в плейлист:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could add a song:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность изменять порядок песен в плейлисте",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to reorder a song within the playlist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут изменить порядок песен:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could reorder the songs:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Возможность удалить песню из плейлиста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Power to delete a song from the playlist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эти группы могут удалить песню:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "These groups could delete a song:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Импорт из текста",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Import from text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Импорт из файла",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Import from file"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Пожалуйста, вставьте данные идентификатора",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Please paste your identity data here"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Выбрать файл",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Select file"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Файл не выбран",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "No file selected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Еще не реализовано",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Not implemented yet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Эта функция еще не реализована!",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "This function hasn't been implemented yet!"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Добавление клиента в группу сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Add client to server group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите уникальный идентификатор клиента или идентификатор базы данных",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter the client unique id or database id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Создание новой группы сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Create a new group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Имя группы",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Group name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Недопустимое имя группы",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Invalid group name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Создать группу с использованием следующего типа:",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Created group should have the type:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Создать",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Создать группу сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Create a server group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Переименовать группу сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Rename server group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Копировать группу сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Duplicate server group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Удалить группу сервера",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Delete server group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Создать группу канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Create a channel group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Переименовать группу канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Rename channel group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Копировать группу канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Duplicate channel group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Удалить группу канала",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Delete channel group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Группа шаблона",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Template group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Обычная группа",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Regular group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Группа ServerQuery",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "ServerQuery group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Переименовать группу",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Rename group"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите новое имя группы",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter the new group name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Введите новое имя группы",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Enter the new group name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"translated": "Вы действительно хотите удалить группу ",
|
||||
"flags": [
|
||||
"google-translate"
|
||||
],
|
||||
"key": {
|
||||
"message": "Do you really want to delete the group "
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" enable-background="new 0 0 512.418 512.418" height="512" viewBox="0 0 512.418 512.418" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m437.335 75.082c-100.1-100.102-262.136-100.118-362.252 0-100.103 100.102-100.118 262.136 0 362.253 100.1 100.102 262.136 100.117 362.252 0 100.103-100.102 100.117-262.136 0-362.253zm-10.706 325.739c-11.968-10.702-24.77-20.173-38.264-28.335 8.919-30.809 14.203-64.712 15.452-99.954h75.309c-3.405 47.503-21.657 92.064-52.497 128.289zm-393.338-128.289h75.309c1.249 35.242 6.533 69.145 15.452 99.954-13.494 8.162-26.296 17.633-38.264 28.335-30.84-36.225-49.091-80.786-52.497-128.289zm52.498-160.936c11.968 10.702 24.77 20.173 38.264 28.335-8.919 30.809-14.203 64.712-15.452 99.954h-75.31c3.406-47.502 21.657-92.063 52.498-128.289zm154.097 31.709c-26.622-1.904-52.291-8.461-76.088-19.278 13.84-35.639 39.354-78.384 76.088-88.977zm0 32.708v63.873h-98.625c1.13-29.812 5.354-58.439 12.379-84.632 27.043 11.822 56.127 18.882 86.246 20.759zm0 96.519v63.873c-30.119 1.877-59.203 8.937-86.246 20.759-7.025-26.193-11.249-54.82-12.379-84.632zm0 96.581v108.254c-36.732-10.593-62.246-53.333-76.088-88.976 23.797-10.817 49.466-17.374 76.088-19.278zm32.646 0c26.622 1.904 52.291 8.461 76.088 19.278-13.841 35.64-39.354 78.383-76.088 88.976zm0-32.708v-63.873h98.625c-1.13 29.812-5.354 58.439-12.379 84.632-27.043-11.822-56.127-18.882-86.246-20.759zm0-96.519v-63.873c30.119-1.877 59.203-8.937 86.246-20.759 7.025 26.193 11.249 54.82 12.379 84.632zm0-96.581v-108.254c36.734 10.593 62.248 53.338 76.088 88.977-23.797 10.816-49.466 17.373-76.088 19.277zm73.32-91.957c20.895 9.15 40.389 21.557 57.864 36.951-8.318 7.334-17.095 13.984-26.26 19.931-8.139-20.152-18.536-39.736-31.604-56.882zm-210.891 56.882c-9.165-5.947-17.941-12.597-26.26-19.931 17.475-15.394 36.969-27.801 57.864-36.951-13.068 17.148-23.465 36.732-31.604 56.882zm.001 295.958c8.138 20.151 18.537 39.736 31.604 56.882-20.895-9.15-40.389-21.557-57.864-36.951 8.318-7.334 17.095-13.984 26.26-19.931zm242.494 0c9.165 5.947 17.942 12.597 26.26 19.93-17.475 15.394-36.969 27.801-57.864 36.951 13.067-17.144 23.465-36.729 31.604-56.881zm26.362-164.302c-1.249-35.242-6.533-69.146-15.452-99.954 13.494-8.162 26.295-17.633 38.264-28.335 30.84 36.225 49.091 80.786 52.497 128.289z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="regular" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m12 10c-.009 0-.019 0-.028 0-.226-.009-.436-.119-.572-.3l-1.275-1.7h-2.375c-.965 0-1.75-.785-1.75-1.75v-4.5c0-.965.785-1.75 1.75-1.75h8.5c.965 0 1.75.785 1.75 1.75v4.5c0 .965-.785 1.75-1.75 1.75h-2.16l-1.526 1.744c-.142.163-.348.256-.564.256zm-4.25-8.5c-.138 0-.25.112-.25.25v4.5c0 .138.112.25.25.25h2.75c.236 0 .458.111.6.3l.945 1.26 1.141-1.303c.142-.164.348-.257.564-.257h2.5c.138 0 .25-.112.25-.25v-4.5c0-.138-.112-.25-.25-.25z"/></g><g><path d="m20 11c-.01 0-.02 0-.029 0-.227-.009-.437-.12-.572-.301l-.79-1.06c-.248-.332-.179-.802.153-1.05.333-.248.802-.179 1.05.153l.235.316 1.139-1.301c.142-.164.348-.257.564-.257h.5c.136 0 .25-.114.25-.25v-4.5c0-.136-.114-.25-.25-.25h-1.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h1.5c.965 0 1.75.785 1.75 1.75v4.5c0 .965-.785 1.75-1.75 1.75h-.16l-1.526 1.744c-.142.163-.348.256-.564.256z"/></g><g><path d="m4 11c-.216 0-.422-.093-.564-.256l-1.526-1.744h-.16c-.965 0-1.75-.785-1.75-1.75v-4.5c0-.965.785-1.75 1.75-1.75h1.5c.414 0 .75.336.75.75s-.336.75-.75.75h-1.5c-.136 0-.25.114-.25.25v4.5c0 .136.114.25.25.25h.5c.216 0 .422.093.564.256l1.139 1.301.235-.316c.248-.332.717-.401 1.05-.153.332.248.401.718.153 1.05l-.79 1.06c-.135.182-.345.293-.572.302-.009 0-.019 0-.029 0z"/></g><g><path d="m20 17c-1.103 0-2-.897-2-2s.897-2 2-2 2 .897 2 2-.897 2-2 2zm0-2.5c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z"/></g><g><path d="m23.25 22c-.414 0-.75-.336-.75-.75v-.5c0-.689-.561-1.25-1.25-1.25h-2.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75h2.5c1.517 0 2.75 1.233 2.75 2.75v.5c0 .414-.336.75-.75.75z"/></g><g><path d="m4 17c-1.103 0-2-.897-2-2s.897-2 2-2 2 .897 2 2-.897 2-2 2zm0-2.5c-.276 0-.5.224-.5.5s.224.5.5.5.5-.224.5-.5-.224-.5-.5-.5z"/></g><g><path d="m.75 22c-.414 0-.75-.336-.75-.75v-.5c0-1.517 1.233-2.75 2.75-2.75h2.5c.414 0 .75.336.75.75s-.336.75-.75.75h-2.5c-.689 0-1.25.561-1.25 1.25v.5c0 .414-.336.75-.75.75z"/></g><g><path d="m12 17.5c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zm0-4.5c-.827 0-1.5.673-1.5 1.5s.673 1.5 1.5 1.5 1.5-.673 1.5-1.5-.673-1.5-1.5-1.5z"/></g><g><path d="m16.75 24c-.414 0-.75-.336-.75-.75v-1.5c0-.689-.561-1.25-1.25-1.25h-5.5c-.689 0-1.25.561-1.25 1.25v1.5c0 .414-.336.75-.75.75s-.75-.336-.75-.75v-1.5c0-1.517 1.233-2.75 2.75-2.75h5.5c1.517 0 2.75 1.233 2.75 2.75v1.5c0 .414-.336.75-.75.75z"/></g></svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,0C114.841,0,0,114.841,0,256s114.841,256,256,256s256-114.841,256-256S397.159,0,256,0z M256,468.732
|
||||
c-117.301,0-212.732-95.431-212.732-212.732S138.699,43.268,256,43.268S468.732,138.699,468.732,256S373.301,468.732,256,468.732z
|
||||
"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M372.101,248.068H271.144V97.176c0-11.948-9.686-21.634-21.634-21.634c-11.948,0-21.634,9.686-21.634,21.634v172.525
|
||||
c0,11.948,9.686,21.634,21.634,21.634c0.244,0,0.48-0.029,0.721-0.036c0.241,0.009,0.477,0.036,0.721,0.036h121.149
|
||||
c11.948,0,21.634-9.686,21.634-21.634S384.049,248.068,372.101,248.068z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,70 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M66.065,313.806C29.635,313.806,0,343.442,0,379.871s29.635,66.065,66.065,66.065s66.065-29.635,66.065-66.065 S102.494,313.806,66.065,313.806z M66.065,412.903c-18.217,0-33.032-14.815-33.032-33.032s14.815-33.032,33.032-33.032 s33.032,14.815,33.032,33.032S84.282,412.903,66.065,412.903z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M176.172,126.624c-36.429,0-66.065,29.635-66.065,66.065s29.635,66.065,66.065,66.065s66.065-29.635,66.065-66.065 S212.601,126.624,176.172,126.624z M176.172,225.72c-18.217,0-33.032-14.815-33.032-33.032s14.815-33.032,33.032-33.032 s33.032,14.815,33.032,33.032S194.389,225.72,176.172,225.72z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M341.333,286.28c-36.429,0-66.065,29.635-66.065,66.065c0,36.429,29.635,66.065,66.065,66.065 s66.065-29.635,66.065-66.065C407.398,315.915,377.762,286.28,341.333,286.28z M341.333,385.376 c-18.217,0-33.032-14.815-33.032-33.032c0-18.217,14.815-33.032,33.032-33.032s33.032,14.815,33.032,33.032 C374.366,370.561,359.551,385.376,341.333,385.376z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M445.935,66.065c-36.429,0-66.065,29.635-66.065,66.065s29.635,66.065,66.065,66.065S512,168.558,512,132.129 S482.365,66.065,445.935,66.065z M445.935,165.161c-18.217,0-33.032-14.815-33.032-33.032s14.815-33.032,33.032-33.032 s33.032,14.815,33.032,33.032S464.153,165.161,445.935,165.161z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
|
||||
<rect x="61.58" y="269.761" transform="matrix(0.5547 -0.8321 0.8321 0.5547 -184.2592 228.275)" width="119.103" height="33.034"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
|
||||
<rect x="242.213" y="208.023" transform="matrix(0.6402 -0.7682 0.7682 0.6402 -116.2621 296.8083)" width="33.032" height="128.996"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
|
||||
<rect x="319.551" y="222.711" transform="matrix(0.4672 -0.8842 0.8842 0.4672 -2.9392 473.5728)" width="143.807" height="33.028"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 480 480" style="enable-background:new 0 0 480 480;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M0,32v416h480V32H0z M448,416H32V64h416V416z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="208" y="288" width="128" height="32"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="123.36,132.64 102.24,153.76 172.64,224 102.24,294.24 123.36,315.36 214.56,224 "/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 783 B |
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 480 480" style="enable-background:new 0 0 480 480;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M278.944,17.577c-5.568-2.656-12.128-1.952-16.928,1.92L106.368,144.009H32c-17.632,0-32,14.368-32,32v128
|
||||
c0,17.632,14.368,32,32,32h74.368l155.616,124.512c2.912,2.304,6.464,3.488,10.016,3.488c2.368,0,4.736-0.512,6.944-1.568
|
||||
c5.536-2.688,9.056-8.288,9.056-14.432v-416C288,25.865,284.48,20.265,278.944,17.577z M96,304.009H32v-128h64V304.009z
|
||||
M256,414.697l-128-102.4V167.721l128-102.4V414.697z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M369.024,126.857c-6.304-6.24-16.416-6.144-22.624,0.128c-6.208,6.304-6.144,16.416,0.128,22.624
|
||||
c24.16,23.904,37.472,56,37.472,90.4c0,34.4-13.312,66.496-37.472,90.4c-6.304,6.208-6.368,16.32-0.128,22.624
|
||||
c3.136,3.168,7.264,4.736,11.36,4.736c4.064,0,8.128-1.536,11.264-4.64c30.304-29.92,46.976-70.08,46.976-113.12
|
||||
C416,196.969,399.328,156.809,369.024,126.857z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M414.144,81.769c-6.272-6.208-16.416-6.176-22.624,0.096c-6.208,6.272-6.176,16.416,0.096,22.624
|
||||
C427.968,140.553,448,188.681,448,240.009s-20.032,99.456-56.384,135.52c-6.272,6.208-6.304,16.352-0.096,22.624
|
||||
c3.136,3.168,7.232,4.736,11.36,4.736c4.064,0,8.128-1.536,11.264-4.64C456.608,356.137,480,299.945,480,240.009
|
||||
C480,180.073,456.608,123.881,414.144,81.769z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -2,9 +2,10 @@
|
|||
'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:#151515;}
|
||||
.st0{fill:#666666;}
|
||||
</style>
|
||||
<g><g id="Icon-Plus" transform="translate(28.000000, 278.000000)"><path class="st0" d="M4-222.1c-13.2,0-23.9-10.7-23.9-23.9c0-13.2,10.7-23.9,23.9-23.9s23.9,10.7,23.9,23.9 C27.9-232.8,17.2-222.1,4-222.1L4-222.1z M4-267.3c-11.7,0-21.3,9.6-21.3,21.3s9.6,21.3,21.3,21.3s21.3-9.6,21.3-21.3 S15.7-267.3,4-267.3L4-267.3z" id="Fill-38"/><polygon
|
||||
<g><g id="Icon-Plus" transform="translate(28.000000, 278.000000)">
|
||||
<path class="st0" d="M4-222.1c-13.2,0-23.9-10.7-23.9-23.9c0-13.2,10.7-23.9,23.9-23.9s23.9,10.7,23.9,23.9 C27.9-232.8,17.2-222.1,4-222.1L4-222.1z M4-267.3c-11.7,0-21.3,9.6-21.3,21.3s9.6,21.3,21.3,21.3s21.3-9.6,21.3-21.3 S15.7-267.3,4-267.3L4-267.3z" id="Fill-38"/><polygon
|
||||
class="st0" id="Fill-39" points="-8.7,-247.4 16.7,-247.4 16.7,-244.6 -8.7,-244.6 "/><polygon class="st0"
|
||||
id="Fill-40"
|
||||
points="2.6,-258.7 5.4,-258.7 5.4,-233.3 2.6,-233.3 "/></g></g></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -1,3 +1,3 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:#151515;}
|
||||
.st0{fill:#666666;}
|
||||
</style><g><g id="Icon-Trash" transform="translate(232.000000, 228.000000)"><polygon class="st0" id="Fill-6" points="-207.5,-205.1 -204.5,-205.1 -204.5,-181.1 -207.5,-181.1 "/><polygon class="st0" id="Fill-7" points="-201.5,-205.1 -198.5,-205.1 -198.5,-181.1 -201.5,-181.1 "/><polygon class="st0" id="Fill-8" points="-195.5,-205.1 -192.5,-205.1 -192.5,-181.1 -195.5,-181.1 "/><polygon class="st0" id="Fill-9" points="-219.5,-214.1 -180.5,-214.1 -180.5,-211.1 -219.5,-211.1 "/><path class="st0" d="M-192.6-212.6h-2.8v-3c0-0.9-0.7-1.6-1.6-1.6h-6c-0.9,0-1.6,0.7-1.6,1.6v3h-2.8v-3 c0-2.4,2-4.4,4.4-4.4h6c2.4,0,4.4,2,4.4,4.4V-212.6" id="Fill-10"/><path class="st0" d="M-191-172.1h-18c-2.4,0-4.5-2-4.7-4.4l-2.8-36l3-0.2l2.8,36c0.1,0.9,0.9,1.6,1.7,1.6h18 c0.9,0,1.7-0.8,1.7-1.6l2.8-36l3,0.2l-2.8,36C-186.5-174-188.6-172.1-191-172.1" id="Fill-11"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -3,7 +3,7 @@
|
|||
<svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512"
|
||||
width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<style type="text/css">
|
||||
.st0{fill:#151515;}
|
||||
.st0{fill:#666666;}
|
||||
</style>
|
||||
<g><polygon class="st0" points="304,96 288,96 288,176 368,176 368,160 304,160 "/><path class="st0"
|
||||
d="M325.3,64H160v48h-48v336h240v-48h48V139L325.3,64z M336,432H128V128h32v272h176V432z M384,384H176V80h142.7l65.3,65.6V384 z"/></g></svg>
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
|
@ -1,3 +1,3 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg id="Layer_1" style="enable-background:new 0 0 64 64;" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:#151515;}
|
||||
.st0{fill:#666666;}
|
||||
</style><g><g id="Icon-Pencil" transform="translate(179.000000, 382.000000)"><path class="st0" d="M-168.2-328l3.7-14.9l22.7-22.7l11.2,11.2l-22.7,22.7L-168.2-328L-168.2-328z M-161.9-341.5 l-2.4,9.6l9.6-2.4l20.2-20.2l-7.2-7.2L-161.9-341.5L-161.9-341.5z" id="Fill-168"/><path class="st0" d="M-155.7-332.6c-1-3.9-4-6.9-7.9-7.9l0.7-2.8c4.9,1.2,8.7,5,9.9,9.9L-155.7-332.6" id="Fill-169"/><polyline class="st0" id="Fill-170" points="-156,-338.1 -158,-340.2 -138.1,-360.1 -136.1,-358.1 -156,-338.1 "/><path class="st0" d="M-166.2-330l4.4-1.1c-0.4-1.6-1.7-2.9-3.3-3.3L-166.2-330" id="Fill-171"/><path class="st0" d="M-129.5-355.5l-11.2-11.2l4.5-4.5l0.7,0.1c5.4,0.7,9.7,5,10.4,10.4l0.1,0.7L-129.5-355.5 L-129.5-355.5z M-136.6-366.7l7.2,7.2l1.4-1.4c-0.8-3.6-3.6-6.4-7.2-7.2L-136.6-366.7L-136.6-366.7z" id="Fill-172"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" fill="#7289da" enable-background="new 0 0 512.418 512.418" height="512" viewBox="0 0 512.418 512.418" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m437.335 75.082c-100.1-100.102-262.136-100.118-362.252 0-100.103 100.102-100.118 262.136 0 362.253 100.1 100.102 262.136 100.117 362.252 0 100.103-100.102 100.117-262.136 0-362.253zm-10.706 325.739c-11.968-10.702-24.77-20.173-38.264-28.335 8.919-30.809 14.203-64.712 15.452-99.954h75.309c-3.405 47.503-21.657 92.064-52.497 128.289zm-393.338-128.289h75.309c1.249 35.242 6.533 69.145 15.452 99.954-13.494 8.162-26.296 17.633-38.264 28.335-30.84-36.225-49.091-80.786-52.497-128.289zm52.498-160.936c11.968 10.702 24.77 20.173 38.264 28.335-8.919 30.809-14.203 64.712-15.452 99.954h-75.31c3.406-47.502 21.657-92.063 52.498-128.289zm154.097 31.709c-26.622-1.904-52.291-8.461-76.088-19.278 13.84-35.639 39.354-78.384 76.088-88.977zm0 32.708v63.873h-98.625c1.13-29.812 5.354-58.439 12.379-84.632 27.043 11.822 56.127 18.882 86.246 20.759zm0 96.519v63.873c-30.119 1.877-59.203 8.937-86.246 20.759-7.025-26.193-11.249-54.82-12.379-84.632zm0 96.581v108.254c-36.732-10.593-62.246-53.333-76.088-88.976 23.797-10.817 49.466-17.374 76.088-19.278zm32.646 0c26.622 1.904 52.291 8.461 76.088 19.278-13.841 35.64-39.354 78.383-76.088 88.976zm0-32.708v-63.873h98.625c-1.13 29.812-5.354 58.439-12.379 84.632-27.043-11.822-56.127-18.882-86.246-20.759zm0-96.519v-63.873c30.119-1.877 59.203-8.937 86.246-20.759 7.025 26.193 11.249 54.82 12.379 84.632zm0-96.581v-108.254c36.734 10.593 62.248 53.338 76.088 88.977-23.797 10.816-49.466 17.373-76.088 19.277zm73.32-91.957c20.895 9.15 40.389 21.557 57.864 36.951-8.318 7.334-17.095 13.984-26.26 19.931-8.139-20.152-18.536-39.736-31.604-56.882zm-210.891 56.882c-9.165-5.947-17.941-12.597-26.26-19.931 17.475-15.394 36.969-27.801 57.864-36.951-13.068 17.148-23.465 36.732-31.604 56.882zm.001 295.958c8.138 20.151 18.537 39.736 31.604 56.882-20.895-9.15-40.389-21.557-57.864-36.951 8.318-7.334 17.095-13.984 26.26-19.931zm242.494 0c9.165 5.947 17.942 12.597 26.26 19.93-17.475 15.394-36.969 27.801-57.864 36.951 13.067-17.144 23.465-36.729 31.604-56.881zm26.362-164.302c-1.249-35.242-6.533-69.146-15.452-99.954 13.494-8.162 26.295-17.633 38.264-28.335 30.84 36.225 49.091 80.786 52.497 128.289z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="Flat" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g fill="#8690fa"><circle cx="48" cy="304" r="24"/><circle cx="192" cy="448" r="24"/><circle cx="101" cy="395" r="24"/><circle cx="48" cy="304" r="24"/><path d="m216 448a24 24 0 1 0 -24 24 24 24 0 0 0 24-24"/><circle cx="101" cy="395" r="24"/><circle cx="48" cy="192" r="24"/><circle cx="192" cy="48" r="24"/><circle cx="101" cy="101" r="24"/><circle cx="48" cy="192" r="24"/><path d="m216 48a24 24 0 1 1 -24-24 24 24 0 0 1 24 24"/><circle cx="101" cy="101" r="24"/><path d="m311.992 472a24 24 0 0 1 -6.433-47.123 185.506 185.506 0 0 0 96.328-64.917 181.561 181.561 0 0 0 38.113-111.96c0-81.5-55.349-154.248-134.6-176.922a24 24 0 0 1 13.2-46.147 236.543 236.543 0 0 1 121 82.086 230.506 230.506 0 0 1 .276 282.283 233.819 233.819 0 0 1 -121.421 81.812 24.04 24.04 0 0 1 -6.463.888z"/></g><path d="m456.029 488a24.512 24.512 0 0 1 -2.679-.148l-144-16a24 24 0 0 1 -20.474-30.278l40-144a24 24 0 1 1 46.248 12.848l-32.454 116.838 115.98 12.886a24 24 0 0 1 -2.621 47.854z" fill="#5153ff"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #999;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path id="icon" class="cls-1" d="M200.012,0c110.457,0,200,89.545,200,200s-89.54,200-200,200S0.015,310.456.015,200,89.557,0,200.012,0Zm0.01,38.636A160.629,160.629,0,1,1,39.395,199.268,160.632,160.632,0,0,1,200.022,38.639ZM159.407,153.478s42.867-5.468,19.8,68.014c-24.208,77.113-66.566,140.45,63.467,81.447,0,0-64.556,21.6-42.619-37.407,12.79-34.406,28.292-81.421,27.092-93.4C225.387,154.546,207.6,141.025,159.407,153.478Zm65.564-79a24.685,24.685,0,1,1-24.687,24.684A24.685,24.685,0,0,1,224.971,74.479Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 757 B |
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="forward_2_">
|
||||
<path d="M35.518,24.306l-10.971,8.592C24.329,33.065,24,32.978,24,32.703v-7.971v-0.022l-10.453,8.187
|
||||
C13.329,33.065,13,32.978,13,32.703V15.28c0-0.275,0.329-0.362,0.547-0.194L24,23.242V23.22V15.28c0-0.275,0.329-0.362,0.547-0.194
|
||||
l11.033,8.608C35.798,23.862,35.734,24.138,35.518,24.306z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 669 B |
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path transform='rotate(90, 256, 256)' d="M85.333,213.333h341.333C473.728,213.333,512,175.061,512,128s-38.272-85.333-85.333-85.333H85.333
|
||||
C38.272,42.667,0,80.939,0,128S38.272,213.333,85.333,213.333z"/>
|
||||
<path transform='rotate(90, 256, 256)' d="M426.667,298.667H85.333C38.272,298.667,0,336.939,0,384s38.272,85.333,85.333,85.333h341.333
|
||||
C473.728,469.333,512,431.061,512,384S473.728,298.667,426.667,298.667z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1021 B |
After Width: | Height: | Size: 16 KiB |
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="300px" width="300px" version="1.0" viewBox="-300 -300 600 600" xml:space="preserve">
|
||||
<circle stroke="#AAA" stroke-width="10" r="280" fill="#FFF"/>
|
||||
<text style="letter-spacing:1;text-anchor:middle;text-align:center;stroke-opacity:.5;stroke:#000;stroke-width:2;fill:#444;font-size:360px;font-family:Bitstream Vera Sans,Liberation Sans, Arial, sans-serif;line-height:125%;writing-mode:lr-tb;" transform="scale(.2)">
|
||||
<tspan y="-40" x="8">NO IMAGE</tspan>
|
||||
<tspan y="400" x="8">AVAILABLE</tspan>
|
||||
</text>
|
||||
</svg>
|
Before Width: | Height: | Size: 574 B |
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="playlist_1_">
|
||||
<path d="M37.192,23.032c-0.847,0.339-0.179-0.339-0.179-0.339s1.422-2.092,0.406-3.786c-0.793-1.321-3.338-1.075-4.42-1.669v14.154
|
||||
c0,0.037-0.016,0.07-0.022,0.106c-0.154,1.504-1.607,3.034-3.696,3.712c-2.559,0.829-5.102,0.063-5.678-1.711
|
||||
c-0.574-1.774,1.034-3.887,3.595-4.717c0.66-0.189,2.207-0.439,2.801-0.193V12.607C30,12.273,30.271,12,30.607,12h1.785
|
||||
C32.728,12,33,12.273,33,12.607v0.549c1.542,1.004,6.18,1.455,6.851,4.139C40.656,20.52,38.038,22.693,37.192,23.032z M12.5,20H28
|
||||
v-3H12.5c-0.275,0-0.5,0.225-0.5,0.5v2C12,19.775,12.225,20,12.5,20z M12.5,26H28v-3H12.5c-0.275,0-0.5,0.225-0.5,0.5v2
|
||||
C12,25.775,12.225,26,12.5,26z M22.625,29H12.5c-0.275,0-0.5,0.225-0.5,0.5v2c0,0.275,0.225,0.5,0.5,0.5h8.551
|
||||
C21.227,30.925,21.779,29.887,22.625,29z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<g id="rewind_2_">
|
||||
<path d="M35,15.28v17.423c0,0.274-0.329,0.362-0.547,0.194L24,24.711v0.022v7.971c0,0.274-0.329,0.362-0.547,0.194l-10.971-8.592
|
||||
c-0.217-0.168-0.28-0.443-0.062-0.611l11.033-8.608C23.671,14.918,24,15.005,24,15.28v7.939v0.023l10.453-8.156
|
||||
C34.671,14.918,35,15.005,35,15.28z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 64 KiB |
|
@ -1,14 +1,38 @@
|
|||
/// <reference path="log.ts" />
|
||||
/// <reference path="proto.ts" />
|
||||
/// <reference path="ui/view.ts" />
|
||||
/// <reference path="settings.ts" />
|
||||
/// <reference path="FileManager.ts" />
|
||||
/// <reference path="permission/PermissionManager.ts" />
|
||||
/// <reference path="permission/GroupManager.ts" />
|
||||
/// <reference path="ui/frames/ControlBar.ts" />
|
||||
/// <reference path="connection/ConnectionBase.ts" />
|
||||
import {ChannelTree} from "tc-shared/ui/view";
|
||||
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
|
||||
import {PermissionManager} from "tc-shared/permission/PermissionManager";
|
||||
import {GroupManager} from "tc-shared/permission/GroupManager";
|
||||
import {ServerSettings, Settings, StaticSettings} from "tc-shared/settings";
|
||||
import {Sound, SoundManager} from "tc-shared/sound/Sounds";
|
||||
import {LocalClientEntry} from "tc-shared/ui/client";
|
||||
import {ServerLog} from "tc-shared/ui/frames/server_log";
|
||||
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {ServerAddress} from "tc-shared/ui/server";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||
import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import {hashPassword} from "tc-shared/utils/helpers";
|
||||
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
|
||||
import * as htmltags from "./ui/htmltags";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {InputStartResult, InputState} from "tc-shared/voice/RecorderBase";
|
||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import * as bipc from "./BrowserIPC";
|
||||
import {FileManager, spawn_upload_transfer, UploadKey} from "tc-shared/FileManager";
|
||||
import {RecorderProfile} from "tc-shared/voice/RecorderProfile";
|
||||
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";
|
||||
|
||||
enum DisconnectReason {
|
||||
export enum DisconnectReason {
|
||||
HANDLER_DESTROYED,
|
||||
REQUESTED,
|
||||
DNS_FAILED,
|
||||
|
@ -28,7 +52,7 @@ enum DisconnectReason {
|
|||
UNKNOWN
|
||||
}
|
||||
|
||||
enum ConnectionState {
|
||||
export enum ConnectionState {
|
||||
UNCONNECTED,
|
||||
CONNECTING,
|
||||
INITIALISING,
|
||||
|
@ -36,7 +60,7 @@ enum ConnectionState {
|
|||
DISCONNECTING
|
||||
}
|
||||
|
||||
enum ViewReasonId {
|
||||
export enum ViewReasonId {
|
||||
VREASON_USER_ACTION = 0,
|
||||
VREASON_MOVED = 1,
|
||||
VREASON_SYSTEM = 2,
|
||||
|
@ -51,7 +75,7 @@ enum ViewReasonId {
|
|||
VREASON_SERVER_SHUTDOWN = 11
|
||||
}
|
||||
|
||||
interface VoiceStatus {
|
||||
export interface VoiceStatus {
|
||||
input_hardware: boolean;
|
||||
input_muted: boolean;
|
||||
output_muted: boolean;
|
||||
|
@ -68,7 +92,7 @@ interface VoiceStatus {
|
|||
queries_visible: boolean;
|
||||
}
|
||||
|
||||
interface ConnectParameters {
|
||||
export interface ConnectParameters {
|
||||
nickname?: string;
|
||||
channel?: {
|
||||
target: string | number;
|
||||
|
@ -79,20 +103,21 @@ interface ConnectParameters {
|
|||
auto_reconnect_attempt?: boolean;
|
||||
}
|
||||
|
||||
class ConnectionHandler {
|
||||
declare const native_client;
|
||||
export class ConnectionHandler {
|
||||
channelTree: ChannelTree;
|
||||
|
||||
serverConnection: connection.AbstractServerConnection;
|
||||
serverConnection: AbstractServerConnection;
|
||||
|
||||
fileManager: FileManager;
|
||||
|
||||
permissions: PermissionManager;
|
||||
groups: GroupManager;
|
||||
|
||||
side_bar: chat.Frame;
|
||||
side_bar: Frame;
|
||||
|
||||
settings: ServerSettings;
|
||||
sound: sound.SoundManager;
|
||||
sound: SoundManager;
|
||||
|
||||
hostbanner: Hostbanner;
|
||||
|
||||
|
@ -121,15 +146,15 @@ class ConnectionHandler {
|
|||
};
|
||||
|
||||
invoke_resized_on_activate: boolean = false;
|
||||
log: log.ServerLog;
|
||||
log: ServerLog;
|
||||
|
||||
constructor() {
|
||||
this.settings = new ServerSettings();
|
||||
|
||||
this.log = new log.ServerLog(this);
|
||||
this.log = new ServerLog(this);
|
||||
this.channelTree = new ChannelTree(this);
|
||||
this.side_bar = new chat.Frame(this);
|
||||
this.sound = new sound.SoundManager(this);
|
||||
this.side_bar = new Frame(this);
|
||||
this.sound = new SoundManager(this);
|
||||
this.hostbanner = new Hostbanner(this);
|
||||
|
||||
this.serverConnection = connection.spawn_server_connection(this);
|
||||
|
@ -169,7 +194,7 @@ class ConnectionHandler {
|
|||
|
||||
setup() { }
|
||||
|
||||
async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
|
||||
async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
|
||||
this.tab_set_name(tr("Connecting"));
|
||||
this.cancel_reconnect(false);
|
||||
this._reconnect_attempt = parameters.auto_reconnect_attempt || false;
|
||||
|
@ -192,7 +217,7 @@ class ConnectionHandler {
|
|||
}
|
||||
}
|
||||
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
|
||||
this.log.log(log.server.Type.CONNECTION_BEGIN, {
|
||||
this.log.log(server_log.Type.CONNECTION_BEGIN, {
|
||||
address: {
|
||||
server_hostname: server_address.host,
|
||||
server_port: server_address.port
|
||||
|
@ -203,7 +228,7 @@ class ConnectionHandler {
|
|||
|
||||
if(parameters.password && !parameters.password.hashed){
|
||||
try {
|
||||
const password = await helpers.hashPassword(parameters.password.password);
|
||||
const password = await hashPassword(parameters.password.password);
|
||||
parameters.password = {
|
||||
hashed: true,
|
||||
password: password
|
||||
|
@ -221,9 +246,9 @@ class ConnectionHandler {
|
|||
}
|
||||
|
||||
const original_address = {host: server_address.host, port: server_address.port};
|
||||
if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) {
|
||||
if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
|
||||
const id = ++this._connect_initialize_id;
|
||||
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||
try {
|
||||
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
|
||||
if(id != this._connect_initialize_id)
|
||||
|
@ -231,7 +256,7 @@ class ConnectionHandler {
|
|||
|
||||
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
|
||||
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
|
||||
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVED, {
|
||||
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVED, {
|
||||
address: {
|
||||
server_port: server_address.port,
|
||||
server_hostname: server_address.host
|
||||
|
@ -245,7 +270,7 @@ class ConnectionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters));
|
||||
await this.serverConnection.connect(server_address, new HandshakeHandler(profile, parameters));
|
||||
setTimeout(() => {
|
||||
const connected = this.serverConnection.connected();
|
||||
if(user_action && connected) {
|
||||
|
@ -270,7 +295,7 @@ class ConnectionHandler {
|
|||
return this._clientId;
|
||||
}
|
||||
|
||||
getServerConnection() : connection.AbstractServerConnection { return this.serverConnection; }
|
||||
getServerConnection() : AbstractServerConnection { return this.serverConnection; }
|
||||
|
||||
|
||||
/**
|
||||
|
@ -380,7 +405,7 @@ class ConnectionHandler {
|
|||
|
||||
popup.close(); /* no need, but nicer */
|
||||
|
||||
const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile();
|
||||
const profile = find_profile(properties.connect_profile) || default_profile();
|
||||
const cprops = this.reconnect_properties(profile);
|
||||
this.startConnection(properties.connect_address, profile, true, cprops);
|
||||
});
|
||||
|
@ -418,12 +443,12 @@ class ConnectionHandler {
|
|||
case DisconnectReason.HANDLER_DESTROYED:
|
||||
if(data) {
|
||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
this.log.log(log.server.Type.DISCONNECTED, {});
|
||||
this.log.log(server_log.Type.DISCONNECTED, {});
|
||||
}
|
||||
break;
|
||||
case DisconnectReason.DNS_FAILED:
|
||||
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
|
||||
this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
|
||||
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
|
||||
message: data as any
|
||||
});
|
||||
this.sound.play(Sound.CONNECTION_REFUSED);
|
||||
|
@ -431,7 +456,7 @@ class ConnectionHandler {
|
|||
case DisconnectReason.CONNECT_FAILURE:
|
||||
if(this._reconnect_attempt) {
|
||||
auto_reconnect = true;
|
||||
this.log.log(log.server.Type.CONNECTION_FAILED, {});
|
||||
this.log.log(server_log.Type.CONNECTION_FAILED, {});
|
||||
break;
|
||||
}
|
||||
if(data)
|
||||
|
@ -452,7 +477,7 @@ class ConnectionHandler {
|
|||
|
||||
this._certificate_modal = createErrorModal(
|
||||
tr("Could not connect"),
|
||||
MessageHelper.formatMessage(tr(error_message_format), this.generate_ssl_certificate_accept())
|
||||
formatMessage(/* @tr-ignore */ tr(error_message_format), this.generate_ssl_certificate_accept())
|
||||
);
|
||||
this._certificate_modal.close_listener.push(() => this._certificate_modal = undefined);
|
||||
this._certificate_modal.open();
|
||||
|
@ -470,7 +495,7 @@ class ConnectionHandler {
|
|||
case DisconnectReason.HANDSHAKE_TEAMSPEAK_REQUIRED:
|
||||
createErrorModal(
|
||||
tr("Target server is a TeamSpeak server"),
|
||||
MessageHelper.formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
|
||||
formatMessage(tr("The target server is a TeamSpeak 3 server!{:br:}Only TeamSpeak 3 based identities are able to connect.{:br:}Please select another profile or change the identify type."))
|
||||
).open();
|
||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
auto_reconnect = false;
|
||||
|
@ -478,7 +503,7 @@ class ConnectionHandler {
|
|||
case DisconnectReason.IDENTITY_TOO_LOW:
|
||||
createErrorModal(
|
||||
tr("Identity level is too low"),
|
||||
MessageHelper.formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"])
|
||||
formatMessage(tr("You've been disconnected, because your Identity level is too low.{:br:}You need at least a level of {0}"), data["extra_message"])
|
||||
).open();
|
||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
|
||||
|
@ -506,7 +531,7 @@ class ConnectionHandler {
|
|||
|
||||
break;
|
||||
case DisconnectReason.SERVER_CLOSED:
|
||||
this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg});
|
||||
this.log.log(server_log.Type.SERVER_CLOSED, {message: data.reasonmsg});
|
||||
|
||||
createErrorModal(
|
||||
tr("Server closed"),
|
||||
|
@ -518,7 +543,7 @@ class ConnectionHandler {
|
|||
auto_reconnect = true;
|
||||
break;
|
||||
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
|
||||
this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {});
|
||||
this.log.log(server_log.Type.SERVER_REQUIRES_PASSWORD, {});
|
||||
|
||||
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
|
||||
if(!(typeof password === "string")) return;
|
||||
|
@ -540,7 +565,7 @@ class ConnectionHandler {
|
|||
const have_invoker = typeof(data["invokerid"]) !== "undefined" && parseInt(data["invokerid"]) !== 0;
|
||||
const modal = createErrorModal(
|
||||
tr("You've been kicked"),
|
||||
MessageHelper.formatMessage(
|
||||
formatMessage(
|
||||
have_invoker ? tr("You've been kicked from the server by {0}:{:br:}{1}") : tr("You've been kicked from the server:{:br:}{1}"),
|
||||
have_invoker ?
|
||||
htmltags.generate_client_object({ client_id: parseInt(data["invokerid"]), client_unique_id: data["invokeruid"], client_name: data["invokername"]}) :
|
||||
|
@ -559,7 +584,7 @@ class ConnectionHandler {
|
|||
this.sound.play(Sound.CONNECTION_BANNED);
|
||||
break;
|
||||
case DisconnectReason.CLIENT_BANNED:
|
||||
this.log.log(log.server.Type.SERVER_BANNED, {
|
||||
this.log.log(server_log.Type.SERVER_BANNED, {
|
||||
invoker: {
|
||||
client_name: data["invokername"],
|
||||
client_id: parseInt(data["invokerid"]),
|
||||
|
@ -592,7 +617,7 @@ class ConnectionHandler {
|
|||
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
|
||||
return;
|
||||
}
|
||||
this.log.log(log.server.Type.RECONNECT_SCHEDULED, {timeout: 5000});
|
||||
this.log.log(server_log.Type.RECONNECT_SCHEDULED, {timeout: 5000});
|
||||
|
||||
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
|
||||
const server_address = this.serverConnection.remote_address();
|
||||
|
@ -600,7 +625,7 @@ class ConnectionHandler {
|
|||
|
||||
this._reconnect_timer = setTimeout(() => {
|
||||
this._reconnect_timer = undefined;
|
||||
this.log.log(log.server.Type.RECONNECT_EXECUTE, {});
|
||||
this.log.log(server_log.Type.RECONNECT_EXECUTE, {});
|
||||
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
|
||||
|
||||
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
|
||||
|
@ -610,7 +635,7 @@ class ConnectionHandler {
|
|||
|
||||
cancel_reconnect(log_event: boolean) {
|
||||
if(this._reconnect_timer) {
|
||||
if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {});
|
||||
if(log_event) this.log.log(server_log.Type.RECONNECT_CANCELED, {});
|
||||
clearTimeout(this._reconnect_timer);
|
||||
this._reconnect_timer = undefined;
|
||||
}
|
||||
|
@ -621,6 +646,7 @@ class ConnectionHandler {
|
|||
control_bar.update_connection_state();
|
||||
}
|
||||
|
||||
private _last_record_error_popup: number;
|
||||
update_voice_status(targetChannel?: ChannelEntry) {
|
||||
if(!this._local_client) return; /* we've been destroyed */
|
||||
|
||||
|
@ -664,7 +690,7 @@ class ConnectionHandler {
|
|||
if(Object.keys(property_update).length > 0) {
|
||||
this.serverConnection.send_command("clientupdate", property_update).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
|
||||
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")});
|
||||
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")});
|
||||
|
||||
/* Update these properties anyways (for case the server fails to handle the command) */
|
||||
const updates = [];
|
||||
|
@ -703,20 +729,25 @@ class ConnectionHandler {
|
|||
if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) {
|
||||
const active = !this.client_status.input_muted && !this.client_status.output_muted;
|
||||
/* No need to start the microphone when we're not even connected */
|
||||
if(active && this.serverConnection.connected()) {
|
||||
if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) {
|
||||
vconnection.voice_recorder().input.start().then(result => {
|
||||
if(result != audio.recorder.InputStartResult.EOK) {
|
||||
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), result);
|
||||
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open();
|
||||
}
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
|
||||
createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
|
||||
});
|
||||
|
||||
const input = vconnection.voice_recorder().input;
|
||||
if(input) {
|
||||
if(active && this.serverConnection.connected()) {
|
||||
if(input.current_state() === InputState.PAUSED) {
|
||||
input.start().then(result => {
|
||||
if(result != InputStartResult.EOK)
|
||||
throw result;
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
|
||||
if(Date.now() - (this._last_record_error_popup || 0) > 10 * 1000) {
|
||||
this._last_record_error_popup = Date.now();
|
||||
createErrorModal(tr("Failed to start recording"), formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
input.stop();
|
||||
}
|
||||
} else {
|
||||
vconnection.voice_recorder().input.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -735,7 +766,7 @@ class ConnectionHandler {
|
|||
client_output_hardware: this.client_status.sound_playback_supported
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
|
||||
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
|
||||
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -755,7 +786,7 @@ class ConnectionHandler {
|
|||
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
|
||||
this.log.log(log.server.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
|
||||
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
|
||||
});
|
||||
|
||||
control_bar.update_button_away();
|
||||
|
@ -775,10 +806,10 @@ class ConnectionHandler {
|
|||
});
|
||||
}
|
||||
|
||||
reconnect_properties(profile?: profiles.ConnectionProfile) : ConnectParameters {
|
||||
reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
|
||||
const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
|
||||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") ||
|
||||
settings.static_global(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
|
||||
StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.default_username : undefined) ||
|
||||
"Another TeaSpeak user";
|
||||
const channel = (this.getClient() && this.getClient().currentChannel() ? this.getClient().currentChannel().channelId : 0) ||
|
||||
(this.serverConnection && this.serverConnection.handshake_handler() ? (this.serverConnection.handshake_handler().parameters.channel || {} as any).target : "");
|
||||
|
@ -792,7 +823,7 @@ class ConnectionHandler {
|
|||
}
|
||||
|
||||
update_avatar() {
|
||||
Modals.spawnAvatarUpload(data => {
|
||||
spawnAvatarUpload(data => {
|
||||
if(typeof(data) === "undefined")
|
||||
return;
|
||||
if(data === null) {
|
||||
|
@ -808,16 +839,16 @@ class ConnectionHandler {
|
|||
|
||||
let message;
|
||||
if(error instanceof CommandResult)
|
||||
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
|
||||
message = formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to delete avatar"), message).open();
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
log.info(LogCategory.CLIENT, tr("Uploading new avatar"));
|
||||
(async () => {
|
||||
let key: transfer.UploadKey;
|
||||
let key: UploadKey;
|
||||
try {
|
||||
key = await this.fileManager.upload_file({
|
||||
size: data.byteLength,
|
||||
|
@ -834,28 +865,28 @@ class ConnectionHandler {
|
|||
//TODO: Resolve permission name
|
||||
//i_client_max_avatar_filesize
|
||||
if(error.id == ErrorID.PERMISSION_ERROR) {
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
|
||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]);
|
||||
} else {
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
}
|
||||
}
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transfer.spawn_upload_transfer(key).put_data(data);
|
||||
await spawn_upload_transfer(key).put_data(data);
|
||||
} catch(error) {
|
||||
log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
|
||||
|
||||
let message;
|
||||
if(typeof(error) === "string")
|
||||
message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
|
||||
message = formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error);
|
||||
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
message = formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to upload avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
@ -868,9 +899,9 @@ class ConnectionHandler {
|
|||
|
||||
let message;
|
||||
if(error instanceof CommandResult)
|
||||
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message);
|
||||
if(!message)
|
||||
message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
|
||||
message = formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details"));
|
||||
createErrorModal(tr("Failed to set avatar"), message).open();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,76 +1,81 @@
|
|||
/// <reference path="connection/CommandHandler.ts" />
|
||||
/// <reference path="connection/ConnectionBase.ts" />
|
||||
import * as log from "tc-shared/log";
|
||||
import * as hex from "tc-shared/crypto/hex";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||
|
||||
class FileEntry {
|
||||
export class FileEntry {
|
||||
name: string;
|
||||
datetime: number;
|
||||
type: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
class FileListRequest {
|
||||
export class FileListRequest {
|
||||
path: string;
|
||||
entries: FileEntry[];
|
||||
|
||||
callback: (entries: FileEntry[]) => void;
|
||||
}
|
||||
|
||||
namespace transfer {
|
||||
export interface TransferKey {
|
||||
client_transfer_id: number;
|
||||
server_transfer_id: number;
|
||||
export interface TransferKey {
|
||||
client_transfer_id: number;
|
||||
server_transfer_id: number;
|
||||
|
||||
key: string;
|
||||
key: string;
|
||||
|
||||
file_path: string;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
file_name: string;
|
||||
|
||||
peer: {
|
||||
hosts: string[],
|
||||
port: number;
|
||||
};
|
||||
peer: {
|
||||
hosts: string[],
|
||||
port: number;
|
||||
};
|
||||
|
||||
total_size: number;
|
||||
}
|
||||
|
||||
export interface UploadOptions {
|
||||
name: string;
|
||||
path: string;
|
||||
|
||||
channel?: ChannelEntry;
|
||||
channel_password?: string;
|
||||
|
||||
size: number;
|
||||
overwrite: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadTransfer {
|
||||
get_key() : DownloadKey;
|
||||
|
||||
request_file() : Promise<Response>;
|
||||
}
|
||||
|
||||
export interface UploadTransfer {
|
||||
get_key(): UploadKey;
|
||||
|
||||
put_data(data: BlobPart | File) : Promise<void>;
|
||||
}
|
||||
|
||||
export type DownloadKey = TransferKey;
|
||||
export type UploadKey = TransferKey;
|
||||
|
||||
export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer {
|
||||
return new RequestFileDownload(key);
|
||||
}
|
||||
export function spawn_upload_transfer(key: UploadKey) : UploadTransfer {
|
||||
return new RequestFileUpload(key);
|
||||
}
|
||||
total_size: number;
|
||||
}
|
||||
|
||||
class RequestFileDownload implements transfer.DownloadTransfer {
|
||||
readonly transfer_key: transfer.DownloadKey;
|
||||
export interface UploadOptions {
|
||||
name: string;
|
||||
path: string;
|
||||
|
||||
constructor(key: transfer.DownloadKey) {
|
||||
channel?: ChannelEntry;
|
||||
channel_password?: string;
|
||||
|
||||
size: number;
|
||||
overwrite: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadTransfer {
|
||||
get_key() : DownloadKey;
|
||||
|
||||
request_file() : Promise<Response>;
|
||||
}
|
||||
|
||||
export interface UploadTransfer {
|
||||
get_key(): UploadKey;
|
||||
|
||||
put_data(data: BlobPart | File) : Promise<void>;
|
||||
}
|
||||
|
||||
export type DownloadKey = TransferKey;
|
||||
export type UploadKey = TransferKey;
|
||||
|
||||
export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer {
|
||||
return new RequestFileDownload(key);
|
||||
}
|
||||
export function spawn_upload_transfer(key: UploadKey) : UploadTransfer {
|
||||
return new RequestFileUpload(key);
|
||||
}
|
||||
|
||||
export class RequestFileDownload implements DownloadTransfer {
|
||||
readonly transfer_key: DownloadKey;
|
||||
|
||||
constructor(key: DownloadKey) {
|
||||
this.transfer_key = key;
|
||||
}
|
||||
|
||||
|
@ -97,18 +102,18 @@ class RequestFileDownload implements transfer.DownloadTransfer {
|
|||
return response;
|
||||
}
|
||||
|
||||
get_key(): transfer.DownloadKey {
|
||||
get_key(): DownloadKey {
|
||||
return this.transfer_key;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestFileUpload implements transfer.UploadTransfer {
|
||||
readonly transfer_key: transfer.UploadKey;
|
||||
constructor(key: transfer.DownloadKey) {
|
||||
export class RequestFileUpload implements UploadTransfer {
|
||||
readonly transfer_key: UploadKey;
|
||||
constructor(key: DownloadKey) {
|
||||
this.transfer_key = key;
|
||||
}
|
||||
|
||||
get_key(): transfer.UploadKey {
|
||||
get_key(): UploadKey {
|
||||
return this.transfer_key;
|
||||
}
|
||||
|
||||
|
@ -152,14 +157,14 @@ class RequestFileUpload implements transfer.UploadTransfer {
|
|||
}
|
||||
}
|
||||
|
||||
class FileManager extends connection.AbstractCommandHandler {
|
||||
export class FileManager extends AbstractCommandHandler {
|
||||
handle: ConnectionHandler;
|
||||
icons: IconManager;
|
||||
avatars: AvatarManager;
|
||||
|
||||
private listRequests: FileListRequest[] = [];
|
||||
private pending_download_requests: transfer.DownloadKey[] = [];
|
||||
private pending_upload_requests: transfer.UploadKey[] = [];
|
||||
private pending_download_requests: DownloadKey[] = [];
|
||||
private pending_upload_requests: UploadKey[] = [];
|
||||
|
||||
private transfer_counter : number = 1;
|
||||
|
||||
|
@ -191,7 +196,7 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
this.avatars = undefined;
|
||||
}
|
||||
|
||||
handle_command(command: connection.ServerCommand): boolean {
|
||||
handle_command(command: ServerCommand): boolean {
|
||||
switch (command.command) {
|
||||
case "notifyfilelist":
|
||||
this.notifyFileList(command.arguments);
|
||||
|
@ -276,15 +281,15 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
|
||||
|
||||
/******************************** File download/upload ********************************/
|
||||
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<transfer.DownloadKey> {
|
||||
const transfer_data: transfer.DownloadKey = {
|
||||
download_file(path: string, file: string, channel?: ChannelEntry, password?: string) : Promise<DownloadKey> {
|
||||
const transfer_data: DownloadKey = {
|
||||
file_name: file,
|
||||
file_path: path,
|
||||
client_transfer_id: this.transfer_counter++
|
||||
} as any;
|
||||
|
||||
this.pending_download_requests.push(transfer_data);
|
||||
return new Promise<transfer.DownloadKey>((resolve, reject) => {
|
||||
return new Promise<DownloadKey>((resolve, reject) => {
|
||||
transfer_data["_callback"] = resolve;
|
||||
this.handle.serverConnection.send_command("ftinitdownload", {
|
||||
"path": path,
|
||||
|
@ -301,8 +306,8 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
});
|
||||
}
|
||||
|
||||
upload_file(options: transfer.UploadOptions) : Promise<transfer.UploadKey> {
|
||||
const transfer_data: transfer.UploadKey = {
|
||||
upload_file(options: UploadOptions) : Promise<UploadKey> {
|
||||
const transfer_data: UploadKey = {
|
||||
file_path: options.path,
|
||||
file_name: options.name,
|
||||
client_transfer_id: this.transfer_counter++,
|
||||
|
@ -310,7 +315,7 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
} as any;
|
||||
|
||||
this.pending_upload_requests.push(transfer_data);
|
||||
return new Promise<transfer.UploadKey>((resolve, reject) => {
|
||||
return new Promise<UploadKey>((resolve, reject) => {
|
||||
transfer_data["_callback"] = resolve;
|
||||
this.handle.serverConnection.send_command("ftinitupload", {
|
||||
"path": options.path,
|
||||
|
@ -333,7 +338,7 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
json = json[0];
|
||||
|
||||
let clientftfid = parseInt(json["clientftfid"]);
|
||||
let transfer: transfer.DownloadKey;
|
||||
let transfer: DownloadKey;
|
||||
for(let e of this.pending_download_requests)
|
||||
if(e.client_transfer_id == clientftfid) {
|
||||
transfer = e;
|
||||
|
@ -355,14 +360,14 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
|
||||
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
|
||||
|
||||
(transfer["_callback"] as (val: transfer.DownloadKey) => void)(transfer);
|
||||
(transfer["_callback"] as (val: DownloadKey) => void)(transfer);
|
||||
this.pending_download_requests.remove(transfer);
|
||||
}
|
||||
|
||||
private notifyStartUpload(json) {
|
||||
json = json[0];
|
||||
|
||||
let transfer: transfer.UploadKey;
|
||||
let transfer: UploadKey;
|
||||
let clientftfid = parseInt(json["clientftfid"]);
|
||||
for(let e of this.pending_upload_requests)
|
||||
if(e.client_transfer_id == clientftfid) {
|
||||
|
@ -384,7 +389,7 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
if(transfer.peer.hosts[0].length == 0 || transfer.peer.hosts[0] == '0.0.0.0')
|
||||
transfer.peer.hosts[0] = this.handle.serverConnection.remote_address().host;
|
||||
|
||||
(transfer["_callback"] as (val: transfer.UploadKey) => void)(transfer);
|
||||
(transfer["_callback"] as (val: UploadKey) => void)(transfer);
|
||||
this.pending_upload_requests.remove(transfer);
|
||||
}
|
||||
|
||||
|
@ -411,12 +416,12 @@ class FileManager extends connection.AbstractCommandHandler {
|
|||
}
|
||||
}
|
||||
|
||||
class Icon {
|
||||
export class Icon {
|
||||
id: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
enum ImageType {
|
||||
export enum ImageType {
|
||||
UNKNOWN,
|
||||
BITMAP,
|
||||
PNG,
|
||||
|
@ -425,7 +430,7 @@ enum ImageType {
|
|||
JPEG
|
||||
}
|
||||
|
||||
function media_image_type(type: ImageType, file?: boolean) {
|
||||
export function media_image_type(type: ImageType, file?: boolean) {
|
||||
switch (type) {
|
||||
case ImageType.BITMAP:
|
||||
return "bmp";
|
||||
|
@ -442,7 +447,7 @@ function media_image_type(type: ImageType, file?: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
|
||||
export function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) {
|
||||
const ab2str10 = () => {
|
||||
const buf = new Uint8Array(encoded_data as ArrayBuffer);
|
||||
if(buf.byteLength < 10)
|
||||
|
@ -472,7 +477,7 @@ function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean
|
|||
return ImageType.UNKNOWN;
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
export class CacheManager {
|
||||
readonly cache_name: string;
|
||||
|
||||
private _cache_category: Cache;
|
||||
|
@ -547,7 +552,7 @@ class CacheManager {
|
|||
}
|
||||
}
|
||||
|
||||
class IconManager {
|
||||
export class IconManager {
|
||||
private static cache: CacheManager = new CacheManager("icons");
|
||||
|
||||
handle: FileManager;
|
||||
|
@ -590,7 +595,7 @@ class IconManager {
|
|||
return this.handle.requestFileList("/icons");
|
||||
}
|
||||
|
||||
create_icon_download(id: number) : Promise<transfer.DownloadKey> {
|
||||
create_icon_download(id: number) : Promise<DownloadKey> {
|
||||
return this.handle.download_file("", "/icon_" + id);
|
||||
}
|
||||
|
||||
|
@ -665,7 +670,7 @@ class IconManager {
|
|||
|
||||
private async _load_icon(id: number) : Promise<Icon> {
|
||||
try {
|
||||
let download_key: transfer.DownloadKey;
|
||||
let download_key: DownloadKey;
|
||||
try {
|
||||
download_key = await this.create_icon_download(id);
|
||||
} catch(error) {
|
||||
|
@ -673,7 +678,7 @@ class IconManager {
|
|||
throw "Failed to request icon";
|
||||
}
|
||||
|
||||
const downloader = transfer.spawn_download_transfer(download_key);
|
||||
const downloader = spawn_download_transfer(download_key);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await downloader.request_file();
|
||||
|
@ -801,14 +806,14 @@ class IconManager {
|
|||
}
|
||||
}
|
||||
|
||||
class Avatar {
|
||||
export class Avatar {
|
||||
client_avatar_id: string; /* the base64 uid thing from a-m */
|
||||
avatar_id: string; /* client_flag_avatar */
|
||||
url: string;
|
||||
type: ImageType;
|
||||
}
|
||||
|
||||
class AvatarManager {
|
||||
export class AvatarManager {
|
||||
handle: FileManager;
|
||||
|
||||
private static cache: CacheManager;
|
||||
|
@ -867,14 +872,14 @@ class AvatarManager {
|
|||
};
|
||||
}
|
||||
|
||||
create_avatar_download(client_avatar_id: string) : Promise<transfer.DownloadKey> {
|
||||
create_avatar_download(client_avatar_id: string) : Promise<DownloadKey> {
|
||||
log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id);
|
||||
return this.handle.download_file("", "/avatar_" + client_avatar_id);
|
||||
}
|
||||
|
||||
private async _load_avatar(client_avatar_id: string, avatar_version: string) {
|
||||
try {
|
||||
let download_key: transfer.DownloadKey;
|
||||
let download_key: DownloadKey;
|
||||
try {
|
||||
download_key = await this.create_avatar_download(client_avatar_id);
|
||||
} catch(error) {
|
||||
|
@ -882,7 +887,7 @@ class AvatarManager {
|
|||
throw "failed to request avatar download";
|
||||
}
|
||||
|
||||
const downloader = transfer.spawn_download_transfer(download_key);
|
||||
const downloader = spawn_download_transfer(download_key);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await downloader.request_file();
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
import {Settings, settings} from "tc-shared/settings";
|
||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import * as loader from "tc-loader";
|
||||
import * as image_preview from "./ui/frames/image_preview"
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
declare const xbbcode;
|
||||
export namespace bbcode {
|
||||
const sanitizer_escaped = (key: string) => "[-- sescaped: " + key + " --]";
|
||||
const sanitizer_escaped_regex = /\[-- sescaped: ([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) --]/;
|
||||
const sanitizer_escaped_map: {[key: string]: string} = {};
|
||||
|
||||
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||
|
||||
export interface FormatSettings {
|
||||
is_chat_message?: boolean
|
||||
}
|
||||
|
||||
export function format(message: string, fsettings?: FormatSettings) : JQuery[] {
|
||||
fsettings = fsettings || {};
|
||||
|
||||
single_url_parse:
|
||||
if(fsettings.is_chat_message) {
|
||||
/* try if its only one url */
|
||||
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw_url);
|
||||
} catch(error) {
|
||||
break single_url_parse;
|
||||
}
|
||||
|
||||
single_url_yt:
|
||||
{
|
||||
const result = raw_url.match(yt_url_regex);
|
||||
if(!result) break single_url_yt;
|
||||
|
||||
return format("[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]");
|
||||
}
|
||||
|
||||
single_url_image:
|
||||
{
|
||||
const ext_index = url.pathname.lastIndexOf(".");
|
||||
if(ext_index == -1) break single_url_image;
|
||||
|
||||
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
|
||||
if([
|
||||
"jpeg", "jpg",
|
||||
"png", "bmp", "gif",
|
||||
"tiff", "pdf", "svg"
|
||||
].findIndex(e => e === ext_name) == -1) break single_url_image;
|
||||
|
||||
return format("[img]" + message + "[/img]");
|
||||
}
|
||||
}
|
||||
|
||||
const result = xbbcode.parse(message, {
|
||||
tag_whitelist: [
|
||||
"b", "big",
|
||||
"i", "italic",
|
||||
"u", "underlined",
|
||||
"s", "strikethrough",
|
||||
"color",
|
||||
"url",
|
||||
"code",
|
||||
"i-code", "icode",
|
||||
"sub", "sup",
|
||||
"size",
|
||||
"hr", "br",
|
||||
"left", "l", "center", "c", "right", "r",
|
||||
|
||||
"ul", "ol", "list",
|
||||
"li",
|
||||
|
||||
"table",
|
||||
"tr", "td", "th",
|
||||
|
||||
"yt", "youtube",
|
||||
"img"
|
||||
]
|
||||
});
|
||||
let html = result.build_html();
|
||||
if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||
html = twemoji.parse(html);
|
||||
|
||||
const container = $.spawn("div");
|
||||
let sanitized = DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: [
|
||||
"x-highlight-type",
|
||||
"x-code-type",
|
||||
"x-image-url"
|
||||
]
|
||||
});
|
||||
|
||||
sanitized = sanitized.replace(sanitizer_escaped_regex, data => {
|
||||
const uid = data.match(sanitizer_escaped_regex)[1];
|
||||
const value = sanitizer_escaped_map[uid];
|
||||
if(!value) return data;
|
||||
delete sanitizer_escaped_map[uid];
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
container[0].innerHTML = sanitized;
|
||||
|
||||
|
||||
container.find("a")
|
||||
.attr('target', "_blank")
|
||||
.on('contextmenu', event => {
|
||||
if(event.isDefaultPrevented()) return;
|
||||
event.preventDefault();
|
||||
|
||||
const url = $(event.target).attr("href");
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(url, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open URL"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, {
|
||||
callback: () => {
|
||||
//TODO
|
||||
},
|
||||
name: tr("Open URL in Browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
visible: __build.target === "client" && false // Currently not possible
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(url),
|
||||
name: tr("Copy URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
});
|
||||
});
|
||||
|
||||
return [container.contents() as JQuery];
|
||||
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry));
|
||||
}
|
||||
|
||||
export function load_image(entry: HTMLImageElement) {
|
||||
const url = decodeURIComponent(entry.getAttribute("x-image-url") || "");
|
||||
const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
|
||||
|
||||
entry.onload = undefined;
|
||||
entry.src = proxy_url;
|
||||
|
||||
const parent = $(entry.parentElement);
|
||||
parent.on('contextmenu', event => {
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(url, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open image in browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(url),
|
||||
name: tr("Copy image URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
})
|
||||
});
|
||||
parent.css("cursor", "pointer").on('click', event => image_preview.preview_image(proxy_url, url));
|
||||
}
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
/* override default parser */
|
||||
xbbcode.register.register_parser({
|
||||
tag: ["code", "icode", "i-code"],
|
||||
content_tags_whitelist: [],
|
||||
|
||||
build_html(layer) : string {
|
||||
const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
||||
const language = (layer.options || "").replace("\"", "'").toLowerCase();
|
||||
|
||||
/* remove heading empty lines */
|
||||
let text = layer.content.map(e => e.build_text())
|
||||
.reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "")
|
||||
.replace(/^([ \n\r\t]*)(?=\n)+/g, "");
|
||||
if(text.startsWith("\r") || text.startsWith("\n"))
|
||||
text = text.substr(1);
|
||||
|
||||
let result: HighlightJSResult;
|
||||
if(window.hljs.getLanguage(language))
|
||||
result = window.hljs.highlight(language, text, true);
|
||||
else
|
||||
result = window.hljs.highlightAuto(text);
|
||||
|
||||
let html = '<pre class="' + klass + '">';
|
||||
html += '<code class="hljs" x-code-type="' + language + '" x-highlight-type="' + result.language + '">';
|
||||
html += result.value;
|
||||
return html + "</code></pre>";
|
||||
}
|
||||
});
|
||||
|
||||
/* override the yt parser */
|
||||
const original_parser = xbbcode.register.find_parser("yt");
|
||||
if(original_parser)
|
||||
xbbcode.register.register_parser({
|
||||
tag: ["yt", "youtube"],
|
||||
build_html(layer): string {
|
||||
const result = original_parser.build_html(layer);
|
||||
if(!result.startsWith("<iframe")) return result;
|
||||
|
||||
const url = result.match(/src="(\S+)" /)[1];
|
||||
const uid = guid();
|
||||
|
||||
sanitizer_escaped_map[uid] = "<iframe class=\"xbbcode-tag xbbcode-tag-video\" src=\"" + url + "\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen></iframe>";
|
||||
return sanitizer_escaped(uid);
|
||||
}
|
||||
});
|
||||
|
||||
/* the image parse & displayer */
|
||||
xbbcode.register.register_parser({
|
||||
tag: ["img", "image"],
|
||||
build_html(layer): string {
|
||||
const uid = guid();
|
||||
const fallback_value = "[img]" + layer.build_text() + "[/img]";
|
||||
|
||||
let target;
|
||||
let content = layer.content.map(e => e.build_text()).join("");
|
||||
if (!layer.options) {
|
||||
target = content;
|
||||
} else
|
||||
target = layer.options;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(target);
|
||||
if(!url.hostname) throw "";
|
||||
} catch(error) {
|
||||
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>";
|
||||
return sanitizer_escaped(uid);
|
||||
}
|
||||
})
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitize_text(text: string) : string {
|
||||
return $(DOMPurify.sanitize("<a>" + text + "</a>", {
|
||||
ADD_ATTR: [
|
||||
"x-highlight-type",
|
||||
"x-code-type",
|
||||
"x-image-url"
|
||||
]
|
||||
})).text();
|
||||
}
|
||||
|
||||
export function formatDate(secs: number) : string {
|
||||
let years = Math.floor(secs / (60 * 60 * 24 * 365));
|
||||
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
|
||||
let hours = Math.floor(secs / (60 * 60)) % 24;
|
||||
let minutes = Math.floor(secs / 60) % 60;
|
||||
let seconds = Math.floor(secs % 60);
|
||||
|
||||
let result = "";
|
||||
if(years > 0)
|
||||
result += years + " " + tr("years") + " ";
|
||||
if(years > 0 || days > 0)
|
||||
result += days + " " + tr("days") + " ";
|
||||
if(years > 0 || days > 0 || hours > 0)
|
||||
result += hours + " " + tr("hours") + " ";
|
||||
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
|
||||
result += minutes + " " + tr("minutes") + " ";
|
||||
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
|
||||
result += seconds + " " + tr("seconds") + " ";
|
||||
else
|
||||
result = tr("now") + " ";
|
||||
|
||||
return result.substr(0, result.length - 1);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
enum KeyCode {
|
||||
export enum KeyCode {
|
||||
KEY_CANCEL = 3,
|
||||
KEY_HELP = 6,
|
||||
KEY_BACK_SPACE = 8,
|
||||
|
@ -118,59 +118,57 @@ enum KeyCode {
|
|||
KEY_META = 224
|
||||
}
|
||||
|
||||
namespace ppt {
|
||||
export enum EventType {
|
||||
KEY_PRESS,
|
||||
KEY_RELEASE,
|
||||
KEY_TYPED
|
||||
}
|
||||
export enum EventType {
|
||||
KEY_PRESS,
|
||||
KEY_RELEASE,
|
||||
KEY_TYPED
|
||||
}
|
||||
|
||||
export enum SpecialKey {
|
||||
CTRL,
|
||||
WINDOWS,
|
||||
SHIFT,
|
||||
ALT
|
||||
}
|
||||
export enum SpecialKey {
|
||||
CTRL,
|
||||
WINDOWS,
|
||||
SHIFT,
|
||||
ALT
|
||||
}
|
||||
|
||||
export interface KeyDescriptor {
|
||||
key_code: string;
|
||||
export interface KeyDescriptor {
|
||||
key_code: string;
|
||||
|
||||
key_ctrl: boolean;
|
||||
key_windows: boolean;
|
||||
key_shift: boolean;
|
||||
key_alt: boolean;
|
||||
}
|
||||
key_ctrl: boolean;
|
||||
key_windows: boolean;
|
||||
key_shift: boolean;
|
||||
key_alt: boolean;
|
||||
}
|
||||
|
||||
export interface KeyEvent extends KeyDescriptor {
|
||||
readonly type: EventType;
|
||||
export interface KeyEvent extends KeyDescriptor {
|
||||
readonly type: EventType;
|
||||
|
||||
readonly key: string;
|
||||
}
|
||||
readonly key: string;
|
||||
}
|
||||
|
||||
export interface KeyHook extends KeyDescriptor {
|
||||
cancel: boolean;
|
||||
export interface KeyHook extends KeyDescriptor {
|
||||
cancel: boolean;
|
||||
|
||||
|
||||
callback_press: () => any;
|
||||
callback_release: () => any;
|
||||
}
|
||||
callback_press: () => any;
|
||||
callback_release: () => any;
|
||||
}
|
||||
|
||||
export function key_description(key: KeyDescriptor) {
|
||||
let result = "";
|
||||
if(key.key_shift)
|
||||
result += " + " + tr("Shift");
|
||||
if(key.key_alt)
|
||||
result += " + " + tr("Alt");
|
||||
if(key.key_ctrl)
|
||||
result += " + " + tr("CTRL");
|
||||
if(key.key_windows)
|
||||
result += " + " + tr("Win");
|
||||
export function key_description(key: KeyDescriptor) {
|
||||
let result = "";
|
||||
if(key.key_shift)
|
||||
result += " + " + tr("Shift");
|
||||
if(key.key_alt)
|
||||
result += " + " + tr("Alt");
|
||||
if(key.key_ctrl)
|
||||
result += " + " + tr("CTRL");
|
||||
if(key.key_windows)
|
||||
result += " + " + tr("Win");
|
||||
|
||||
if(!result && !key.key_code)
|
||||
return tr("unset");
|
||||
if(!result && !key.key_code)
|
||||
return tr("unset");
|
||||
|
||||
if(key.key_code)
|
||||
result += " + " + key.key_code;
|
||||
return result.substr(3);
|
||||
}
|
||||
if(key.key_code)
|
||||
result += " + " + key.key_code;
|
||||
return result.substr(3);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace audio {
|
||||
export namespace player {
|
||||
export interface Device {
|
||||
device_id: string;
|
||||
|
||||
driver: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface Device {
|
||||
device_id: string;
|
||||
|
||||
driver: string;
|
||||
name: string;
|
||||
}
|
|
@ -1,262 +1,260 @@
|
|||
namespace bookmarks {
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math
|
||||
.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
|
||||
import {control_bar} from "tc-shared/ui/frames/ControlBar";
|
||||
import * as top_menu from "./ui/frames/MenuBar";
|
||||
|
||||
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
|
||||
const profile = find_profile(mark.connect_profile) || default_profile();
|
||||
if(profile.valid()) {
|
||||
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
|
||||
server_connections.set_active_connection_handler(connection);
|
||||
connection.startConnection(
|
||||
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
profile,
|
||||
true,
|
||||
{
|
||||
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname,
|
||||
password: mark.server_properties.server_password_hash ? {
|
||||
password: mark.server_properties.server_password_hash,
|
||||
hashed: true
|
||||
} : mark.server_properties.server_password ? {
|
||||
hashed: false,
|
||||
password: mark.server_properties.server_password
|
||||
} : undefined
|
||||
}
|
||||
);
|
||||
} else {
|
||||
spawnConnectModal({}, {
|
||||
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
enforce: true
|
||||
}, {
|
||||
profile: profile,
|
||||
enforce: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
|
||||
const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile();
|
||||
if(profile.valid()) {
|
||||
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler();
|
||||
server_connections.set_active_connection_handler(connection);
|
||||
connection.startConnection(
|
||||
mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
profile,
|
||||
true,
|
||||
{
|
||||
nickname: mark.nickname === "Another TeaSpeak user" || !mark.nickname ? profile.connect_username() : mark.nickname,
|
||||
password: mark.server_properties.server_password_hash ? {
|
||||
password: mark.server_properties.server_password_hash,
|
||||
hashed: true
|
||||
} : mark.server_properties.server_password ? {
|
||||
hashed: false,
|
||||
password: mark.server_properties.server_password
|
||||
} : undefined
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Modals.spawnConnectModal({}, {
|
||||
url: mark.server_properties.server_address + ":" + mark.server_properties.server_port,
|
||||
enforce: true
|
||||
}, {
|
||||
profile: profile,
|
||||
enforce: true
|
||||
})
|
||||
}
|
||||
};
|
||||
export interface ServerProperties {
|
||||
server_address: string;
|
||||
server_port: number;
|
||||
server_password_hash?: string;
|
||||
server_password?: string;
|
||||
}
|
||||
|
||||
export interface ServerProperties {
|
||||
server_address: string;
|
||||
server_port: number;
|
||||
server_password_hash?: string;
|
||||
server_password?: string;
|
||||
}
|
||||
export enum BookmarkType {
|
||||
ENTRY,
|
||||
DIRECTORY
|
||||
}
|
||||
|
||||
export enum BookmarkType {
|
||||
ENTRY,
|
||||
DIRECTORY
|
||||
}
|
||||
export interface Bookmark {
|
||||
type: /* BookmarkType.ENTRY */ BookmarkType;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
|
||||
export interface Bookmark {
|
||||
type: /* BookmarkType.ENTRY */ BookmarkType;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
server_properties: ServerProperties;
|
||||
display_name: string;
|
||||
unique_id: string;
|
||||
|
||||
server_properties: ServerProperties;
|
||||
display_name: string;
|
||||
unique_id: string;
|
||||
nickname: string;
|
||||
default_channel?: number | string;
|
||||
default_channel_password_hash?: string;
|
||||
default_channel_password?: string;
|
||||
|
||||
nickname: string;
|
||||
default_channel?: number | string;
|
||||
default_channel_password_hash?: string;
|
||||
default_channel_password?: string;
|
||||
connect_profile: string;
|
||||
|
||||
connect_profile: string;
|
||||
last_icon_id?: number;
|
||||
}
|
||||
|
||||
last_icon_id?: number;
|
||||
}
|
||||
export interface DirectoryBookmark {
|
||||
type: /* BookmarkType.DIRECTORY */ BookmarkType;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
|
||||
export interface DirectoryBookmark {
|
||||
type: /* BookmarkType.DIRECTORY */ BookmarkType;
|
||||
/* readonly */ parent: DirectoryBookmark;
|
||||
readonly content: (Bookmark | DirectoryBookmark)[];
|
||||
unique_id: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
readonly content: (Bookmark | DirectoryBookmark)[];
|
||||
unique_id: string;
|
||||
display_name: string;
|
||||
}
|
||||
interface BookmarkConfig {
|
||||
root_bookmark?: DirectoryBookmark;
|
||||
default_added?: boolean;
|
||||
}
|
||||
|
||||
interface BookmarkConfig {
|
||||
root_bookmark?: DirectoryBookmark;
|
||||
default_added?: boolean;
|
||||
}
|
||||
|
||||
let _bookmark_config: BookmarkConfig;
|
||||
|
||||
function bookmark_config() : BookmarkConfig {
|
||||
if(_bookmark_config)
|
||||
return _bookmark_config;
|
||||
|
||||
let bookmark_json = localStorage.getItem("bookmarks");
|
||||
let bookmarks;
|
||||
try {
|
||||
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
|
||||
} catch(error) {
|
||||
log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
|
||||
bookmarks = {} as any;
|
||||
}
|
||||
|
||||
_bookmark_config = bookmarks;
|
||||
_bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark;
|
||||
|
||||
if(!_bookmark_config.default_added) {
|
||||
_bookmark_config.default_added = true;
|
||||
create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, {
|
||||
server_address: "ts.teaspeak.de",
|
||||
server_port: 9987
|
||||
}, undefined);
|
||||
|
||||
save_config();
|
||||
}
|
||||
|
||||
const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => {
|
||||
entry.parent = parent;
|
||||
if(entry.type === BookmarkType.DIRECTORY)
|
||||
for(const child of (entry as DirectoryBookmark).content)
|
||||
fix_parent(entry as DirectoryBookmark, child);
|
||||
};
|
||||
for(const entry of _bookmark_config.root_bookmark.content)
|
||||
fix_parent(_bookmark_config.root_bookmark, entry);
|
||||
let _bookmark_config: BookmarkConfig;
|
||||
|
||||
function bookmark_config() : BookmarkConfig {
|
||||
if(_bookmark_config)
|
||||
return _bookmark_config;
|
||||
|
||||
let bookmark_json = localStorage.getItem("bookmarks");
|
||||
let bookmarks;
|
||||
try {
|
||||
bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig;
|
||||
} catch(error) {
|
||||
log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error);
|
||||
bookmarks = {} as any;
|
||||
}
|
||||
|
||||
function save_config() {
|
||||
localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => {
|
||||
if(key === "parent")
|
||||
return undefined;
|
||||
return value;
|
||||
}));
|
||||
_bookmark_config = bookmarks;
|
||||
_bookmark_config.root_bookmark = _bookmark_config.root_bookmark || { content: [], display_name: "root", type: BookmarkType.DIRECTORY} as DirectoryBookmark;
|
||||
|
||||
if(!_bookmark_config.default_added) {
|
||||
_bookmark_config.default_added = true;
|
||||
create_bookmark("TeaSpeak official Test-Server", _bookmark_config.root_bookmark, {
|
||||
server_address: "ts.teaspeak.de",
|
||||
server_port: 9987
|
||||
}, undefined);
|
||||
|
||||
save_config();
|
||||
}
|
||||
|
||||
export function bookmarks() : DirectoryBookmark {
|
||||
return bookmark_config().root_bookmark;
|
||||
}
|
||||
const fix_parent = (parent: DirectoryBookmark, entry: Bookmark | DirectoryBookmark) => {
|
||||
entry.parent = parent;
|
||||
if(entry.type === BookmarkType.DIRECTORY)
|
||||
for(const child of (entry as DirectoryBookmark).content)
|
||||
fix_parent(entry as DirectoryBookmark, child);
|
||||
};
|
||||
for(const entry of _bookmark_config.root_bookmark.content)
|
||||
fix_parent(_bookmark_config.root_bookmark, entry);
|
||||
|
||||
export function bookmarks_flat() : Bookmark[] {
|
||||
const result: Bookmark[] = [];
|
||||
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
|
||||
if(bookmark.type == BookmarkType.DIRECTORY)
|
||||
for(const book of (bookmark as DirectoryBookmark).content)
|
||||
_flat(book);
|
||||
else
|
||||
result.push(bookmark as Bookmark);
|
||||
};
|
||||
_flat(bookmark_config().root_bookmark);
|
||||
return result;
|
||||
}
|
||||
return _bookmark_config;
|
||||
}
|
||||
|
||||
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
|
||||
for(const entry of parent.content) {
|
||||
if(entry.unique_id == uuid)
|
||||
return entry;
|
||||
if(entry.type == BookmarkType.DIRECTORY) {
|
||||
const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid);
|
||||
if(result) return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function save_config() {
|
||||
localStorage.setItem("bookmarks", JSON.stringify(bookmark_config(), (key, value) => {
|
||||
if(key === "parent")
|
||||
return undefined;
|
||||
return value;
|
||||
}));
|
||||
}
|
||||
|
||||
export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined {
|
||||
return find_bookmark_recursive(bookmarks(), uuid);
|
||||
}
|
||||
export function bookmarks() : DirectoryBookmark {
|
||||
return bookmark_config().root_bookmark;
|
||||
}
|
||||
|
||||
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
|
||||
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
|
||||
while(!books.length) {
|
||||
const directory = books.pop_front();
|
||||
if(directory.type == BookmarkType.DIRECTORY) {
|
||||
const cast = <DirectoryBookmark>directory;
|
||||
|
||||
if(cast.content.indexOf(bookmark) != -1)
|
||||
return cast;
|
||||
books.push(...cast.content);
|
||||
}
|
||||
}
|
||||
return bookmarks();
|
||||
}
|
||||
|
||||
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
|
||||
const bookmark = {
|
||||
display_name: display_name,
|
||||
server_properties: server_properties,
|
||||
nickname: nickname,
|
||||
type: BookmarkType.ENTRY,
|
||||
connect_profile: "default",
|
||||
unique_id: guid(),
|
||||
parent: directory
|
||||
} as Bookmark;
|
||||
|
||||
directory.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark {
|
||||
const bookmark = {
|
||||
type: BookmarkType.DIRECTORY,
|
||||
|
||||
display_name: name,
|
||||
content: [],
|
||||
unique_id: guid(),
|
||||
parent: parent
|
||||
} as DirectoryBookmark;
|
||||
|
||||
parent.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
//TODO test if the new parent is within the old bookmark
|
||||
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark(bookmark);
|
||||
parent.content.push(bookmark)
|
||||
}
|
||||
|
||||
export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) {
|
||||
save_config(); /* nvm we dont give a fuck... saving everything */
|
||||
}
|
||||
|
||||
function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
const index = parent.content.indexOf(bookmark);
|
||||
if(index != -1)
|
||||
parent.content.remove(bookmark);
|
||||
export function bookmarks_flat() : Bookmark[] {
|
||||
const result: Bookmark[] = [];
|
||||
const _flat = (bookmark: Bookmark | DirectoryBookmark) => {
|
||||
if(bookmark.type == BookmarkType.DIRECTORY)
|
||||
for(const book of (bookmark as DirectoryBookmark).content)
|
||||
_flat(book);
|
||||
else
|
||||
for(const entry of parent.content)
|
||||
if(entry.type == BookmarkType.DIRECTORY)
|
||||
delete_bookmark_recursive(entry as DirectoryBookmark, bookmark)
|
||||
}
|
||||
result.push(bookmark as Bookmark);
|
||||
};
|
||||
_flat(bookmark_config().root_bookmark);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark_recursive(bookmarks(), bookmark)
|
||||
}
|
||||
|
||||
export function add_current_server() {
|
||||
const ch = server_connections.active_connection_handler();
|
||||
if(ch && ch.connected) {
|
||||
const ce = ch.getClient();
|
||||
const name = ce ? ce.clientNickName() : undefined;
|
||||
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
|
||||
if(result) {
|
||||
const bookmark = create_bookmark(result as string, bookmarks(), {
|
||||
server_port: ch.serverConnection.remote_address().port,
|
||||
server_address: ch.serverConnection.remote_address().host,
|
||||
|
||||
server_password: "",
|
||||
server_password_hash: ""
|
||||
}, name);
|
||||
save_bookmark(bookmark);
|
||||
|
||||
control_bar.update_bookmarks();
|
||||
top_menu.rebuild_bookmarks();
|
||||
|
||||
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark {
|
||||
for(const entry of parent.content) {
|
||||
if(entry.unique_id == uuid)
|
||||
return entry;
|
||||
if(entry.type == BookmarkType.DIRECTORY) {
|
||||
const result = find_bookmark_recursive(entry as DirectoryBookmark, uuid);
|
||||
if(result) return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function find_bookmark(uuid: string) : Bookmark | DirectoryBookmark | undefined {
|
||||
return find_bookmark_recursive(bookmarks(), uuid);
|
||||
}
|
||||
|
||||
export function parent_bookmark(bookmark: Bookmark) : DirectoryBookmark {
|
||||
const books: (DirectoryBookmark | Bookmark)[] = [bookmarks()];
|
||||
while(!books.length) {
|
||||
const directory = books.pop_front();
|
||||
if(directory.type == BookmarkType.DIRECTORY) {
|
||||
const cast = <DirectoryBookmark>directory;
|
||||
|
||||
if(cast.content.indexOf(bookmark) != -1)
|
||||
return cast;
|
||||
books.push(...cast.content);
|
||||
}
|
||||
}
|
||||
return bookmarks();
|
||||
}
|
||||
|
||||
export function create_bookmark(display_name: string, directory: DirectoryBookmark, server_properties: ServerProperties, nickname: string) : Bookmark {
|
||||
const bookmark = {
|
||||
display_name: display_name,
|
||||
server_properties: server_properties,
|
||||
nickname: nickname,
|
||||
type: BookmarkType.ENTRY,
|
||||
connect_profile: "default",
|
||||
unique_id: guid(),
|
||||
parent: directory
|
||||
} as Bookmark;
|
||||
|
||||
directory.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
export function create_bookmark_directory(parent: DirectoryBookmark, name: string) : DirectoryBookmark {
|
||||
const bookmark = {
|
||||
type: BookmarkType.DIRECTORY,
|
||||
|
||||
display_name: name,
|
||||
content: [],
|
||||
unique_id: guid(),
|
||||
parent: parent
|
||||
} as DirectoryBookmark;
|
||||
|
||||
parent.content.push(bookmark);
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
//TODO test if the new parent is within the old bookmark
|
||||
export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark(bookmark);
|
||||
parent.content.push(bookmark)
|
||||
}
|
||||
|
||||
export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) {
|
||||
save_config(); /* nvm we dont give a fuck... saving everything */
|
||||
}
|
||||
|
||||
function delete_bookmark_recursive(parent: DirectoryBookmark, bookmark: Bookmark | DirectoryBookmark) {
|
||||
const index = parent.content.indexOf(bookmark);
|
||||
if(index != -1)
|
||||
parent.content.remove(bookmark);
|
||||
else
|
||||
for(const entry of parent.content)
|
||||
if(entry.type == BookmarkType.DIRECTORY)
|
||||
delete_bookmark_recursive(entry as DirectoryBookmark, bookmark)
|
||||
}
|
||||
|
||||
export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
|
||||
delete_bookmark_recursive(bookmarks(), bookmark)
|
||||
}
|
||||
|
||||
export function add_current_server() {
|
||||
const ch = server_connections.active_connection_handler();
|
||||
if(ch && ch.connected) {
|
||||
const ce = ch.getClient();
|
||||
const name = ce ? ce.clientNickName() : undefined;
|
||||
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
|
||||
if(result) {
|
||||
const bookmark = create_bookmark(result as string, bookmarks(), {
|
||||
server_port: ch.serverConnection.remote_address().port,
|
||||
server_address: ch.serverConnection.remote_address().host,
|
||||
|
||||
server_password: "",
|
||||
server_password_hash: ""
|
||||
}, name);
|
||||
save_bookmark(bookmark);
|
||||
|
||||
control_bar.update_bookmarks();
|
||||
top_menu.rebuild_bookmarks();
|
||||
|
||||
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();
|
||||
}
|
||||
}).open();
|
||||
} else {
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
}
|
||||
}
|