Merge branch 'master' into webrtc

This commit is contained in:
Jonas Herzig 2020-05-04 18:55:22 +02:00
commit 2fda5fd158
17 changed files with 9794 additions and 204 deletions

View file

@ -9,7 +9,8 @@ window.mumbleWebConfig = {
'port': true,
'token': true,
'username': true,
'password': true
'password': true,
'channelName': false
},
// Default values for user settings
// You can see your current value by typing `localStorage.getItem('mumble.$setting')` in the web console.

View file

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) -->
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32">
@ -26,35 +27,52 @@
css: { minimal: minimalView }">
<!-- ko with: connectDialog -->
<div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly()">
<div class="dialog-header">
<div id="connect-dialog_title" class="dialog-header">
Connect to Server
</div>
<form data-bind="submit: connect">
<table>
<tr data-bind="if: $root.config.connectDialog.address">
<td>Address</td>
<td id="connect-dialog_input_address">Address</td>
<td><input id="address" type="text" data-bind="value: address" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.port">
<td>Port</td>
<td id="connect-dialog_input_port">Port</td>
<td><input id="port" type="text" data-bind="value: port" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td>Token</td>
<td><input id="token" type="text" data-bind="value: token"></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.username">
<td>Username</td>
<td id="connect-dialog_input_username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.password">
<td>Password</td>
<td id="connect-dialog_input_password">Password</td>
<td><input id="password" type="password" data-bind="value: password"></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td id="connect-dialog_input_tokens">Tokens</td>
<td>
<input type="text" data-bind='value: tokenToAdd, valueUpdate: "afterkeydown"'>
</td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td></td>
<td>
<button id="connect-dialog_controls_remove" class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button>
<button id="connect-dialog_controls_add" class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button>
</td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token, visible: tokens().length > 0">
<td></td>
<td><select id="token" multiple="multiple" height="5" data-bind="options:tokens, selectedOptions:selectedTokens"></select></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.channelName">
<td>Channel</td>
<td><input id="channelName" type="text" data-bind="value: channelName"></td>
</tr>
</table>
<div class="dialog-footer">
<input class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
<input class="dialog-submit" type="submit" value="Connect">
<input id="connect-dialog_controls_cancel" class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
<input id="connect-dialog_controls_connect" class="dialog-submit" type="submit" value="Connect">
</div>
</form>
</div>
@ -76,46 +94,64 @@
</div>
<form data-bind="submit: connect">
<table>
<tr>
<tr class="reason">
<td colspan=2>
<!-- ko if: type() == 0 || type() == 8 -->
The connection has been refused.
<span class="refused">
The connection has been refused.
</span>
<!-- /ko -->
<!-- ko if: type() == 1 -->
The server uses an incompatible version.
<span class="version">
The server uses an incompatible version.
</span>
<!-- /ko -->
<!-- ko if: type() == 2 -->
Your user name was rejected. Maybe try a different one?
<span class="username">
Your user name was rejected. Maybe try a different one?
</span>
<!-- /ko -->
<!-- ko if: type() == 3 -->
The given password is incorrect.
The user name you have chosen requires a special one.
<span class="userpassword">
The given password is incorrect.
The user name you have chosen requires a special one.
</span>
<!-- /ko -->
<!-- ko if: type() == 4 -->
The given password is incorrect.
<span class="serverpassword">
The given password is incorrect.
</span>
<!-- /ko -->
<!-- ko if: type() == 5 -->
The user name you have chosen is already in use.
<span class="username-in-use">
The user name you have chosen is already in use.
</span>
<!-- /ko -->
<!-- ko if: type() == 6 -->
The server is full.
<span class="full">
The server is full.
</span>
<!-- /ko -->
<!-- ko if: type() == 7 -->
The server requires you to provide a client certificate
which is not supported by this web application.
<span class="clientcert">
The server requires you to provide a client certificate
which is not supported by this web application.
</span>
<!-- /ko -->
<br>
The server reports:
<span class="server">
The server reports:
</span>
<br>
"<span class="connect-error-reason" data-bind="text: reason"></span>"
</td>
</tr>
<tr data-bind="if: type() == 2 || type() == 3 || type() == 5">
<td>Username</td>
<td class="alternate-username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td>
</tr>
<tr data-bind="if: type() == 3 || type() == 4">
<td>Password</td>
<td class="alternate-password">Password</td>
<td><input id="password" type="password" data-bind="value: password" required></td>
</tr>
</table>
@ -266,80 +302,97 @@
<img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView },
click: function () { avatarView(null) }"></img>
<!-- ko with: userContextMenu -->
<ul class="context-menu" data-bind="if: target,
<ul class="context-menu user-context-menu" data-bind="if: target,
style: { left: posX() + 'px',
top: posY() + 'px' }">
<!-- ko with: target -->
<li data-bind="css: { disabled: !canChangeMute() }">
<li data-bind="css: { disabled: !canChangeMute() }"
class="mute">
Mute
</li>
<li data-bind="css: { disabled: !canChangeDeafen() }">
<li data-bind="css: { disabled: !canChangeDeafen() }"
class="deafen">
Deafen
</li>
<li data-bind="css: { disabled: !canChangePrioritySpeaker() }">
<li data-bind="css: { disabled: !canChangePrioritySpeaker() }"
class="priority-speaker">
Priority Speaker
</li>
<li data-bind="css: { disabled: !canLocalMute() }">
<li data-bind="css: { disabled: !canLocalMute() }"
class="local-mute">
Local Mute
</li>
<li data-bind="css: { disabled: !canIgnoreMessages() }">
<li data-bind="css: { disabled: !canIgnoreMessages() }"
class="ignore-messages">
Ignore Messages
</li>
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="view-comment">
View Comment
</li>
<!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: true">
<li data-bind="css: { disabled: !canChangeComment() }, visible: true"
class="change-comment">
Change Comment
</li>
<!-- /ko -->
<!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="reset-comment">
Reset Comment
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: viewAvatar">
click: viewAvatar"
class="view-avatar">
View Avatar
</li>
<!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: true,
click: changeAvatar">
click: changeAvatar"
class="change-avatar">
Change Avatar
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: removeAvatar">
click: removeAvatar",
class="reset-avatar">
Reset Avatar
</li>
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="send-message">
Send Message
</li>
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="information">
Information
</li>
<li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfMute },
click: toggleMute">
click: toggleMute"
class="self-mute">
Self Mute
</li>
<li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfDeaf },
click: toggleDeaf">
click: toggleDeaf"
class="self-deafen">
Self Deafen
</li>
<!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="add-friend">
Add Friend
</li>
<li data-bind="css: { disabled: true }, visible: false">
<li data-bind="css: { disabled: true }, visible: false"
class="remove-friend">
Remove Friend
</li>
<!-- /ko -->
@ -348,43 +401,53 @@
</ul>
<!-- /ko -->
<!-- ko with: channelContextMenu -->
<ul class="context-menu" data-bind="if: target,
<ul class="context-menu channel-context-menu" data-bind="if: target,
style: { left: posX() + 'px',
top: posY() + 'px' }">
<!-- ko with: target -->
<li data-bind="visible: users.indexOf($root.thisUser()) === -1,
css: { disabled: !canJoin() },
click: $root.requestMove.bind($root, $root.thisUser())">
click: $root.requestMove.bind($root, $root.thisUser())"
class="join">
Join Channel
</li>
<li data-bind="css: { disabled: !canAdd() }">
<li data-bind="css: { disabled: !canAdd() }"
class="add">
Add
</li>
<li data-bind="css: { disabled: !canEdit() }">
<li data-bind="css: { disabled: !canEdit() }"
class="edit">
Edit
</li>
<li data-bind="css: { disabled: !canRemove() }">
<li data-bind="css: { disabled: !canRemove() }"
class="remove">
Remove
</li>
<li data-bind="css: { disabled: !canLink() }">
<li data-bind="css: { disabled: !canLink() }"
class="link">
Link
</li>
<li data-bind="css: { disabled: !canUnlink() }">
<li data-bind="css: { disabled: !canUnlink() }"
class="unlink">
Unlink
</li>
<li data-bind="css: { disabled: !canUnlink() }">
<li data-bind="css: { disabled: !canUnlink() }"
class="unlink-all">
Unlink All
</li>
<li data-bind="css: { disabled: true }">
<li data-bind="css: { disabled: true }"
class="copy-mumble-url">
Copy Mumble URL
</li>
<li data-bind="css: { disabled: true }">
<li data-bind="css: { disabled: true }"
class="copy-mumble-web-url">
Copy Mumble-Web URL
</li>
<li data-bind="css: { disabled: !canSendMessage() }">
<li data-bind="css: { disabled: !canSendMessage() }"
class="send-message">
Send Message
</li>

View file

@ -9,6 +9,7 @@ import _dompurify from 'dompurify'
import keyboardjs from 'keyboardjs'
import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
import {initialize as localizationInitialize, translate} from './loc';
const dompurify = _dompurify(window)
@ -50,16 +51,31 @@ function ConnectDialog () {
var self = this
self.address = ko.observable('')
self.port = ko.observable('')
self.token = ko.observable('')
self.tokenToAdd = ko.observable('')
self.selectedTokens = ko.observableArray([])
self.tokens = ko.observableArray([])
self.username = ko.observable('')
self.password = ko.observable('')
self.channelName = ko.observable('')
self.joinOnly = ko.observable(false)
self.visible = ko.observable(true)
self.show = self.visible.bind(self.visible, true)
self.hide = self.visible.bind(self.visible, false)
self.connect = function () {
self.hide()
ui.connect(self.username(), self.address(), self.port(), self.token(), self.password())
ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
}
self.addToken = function() {
if ((self.tokenToAdd() != "") && (self.tokens.indexOf(self.tokenToAdd()) < 0)) {
self.tokens.push(self.tokenToAdd())
}
self.tokenToAdd("")
}
self.removeSelectedTokens = function() {
this.tokens.removeAll(this.selectedTokens())
this.selectedTokens([])
}
}
@ -329,7 +345,7 @@ class GlobalBindings {
return '[' + new Date().toLocaleTimeString('en-US') + ']'
}
this.connect = (username, host, port, token, password) => {
this.connect = (username, host, port, tokens = [], password, channelName = "") => {
this.resetClient()
this.remoteHost(host)
@ -353,7 +369,8 @@ class GlobalBindings {
enabled: true,
mic: this._delayedMicNode.stream,
audioContext: ctx
}
},
tokens: tokens
}).done(client => {
log('Connected!')
@ -367,12 +384,18 @@ class GlobalBindings {
// Make sure we stay open if we're running as Matrix widget
window.matrixWidget.setAlwaysOnScreen(true)
// Register all channels, recursively
const registerChannel = channel => {
this._newChannel(channel)
channel.children.forEach(registerChannel)
// Register all channels, recursively
if(channelName.indexOf("/") != 0) {
channelName = "/"+channelName;
}
registerChannel(client.root)
const registerChannel = (channel, channelPath) => {
this._newChannel(channel)
if(channelPath === channelName) {
client.self.setChannel(channel)
}
channel.children.forEach(ch => registerChannel(ch, channelPath+"/"+ch.name))
}
registerChannel(client.root, "")
// Register all users
client.users.forEach(user => this._newUser(user))
@ -393,6 +416,14 @@ class GlobalBindings {
})
})
// Log permission denied error messages
client.on('denied', (type) => {
ui.log.push({
type: 'generic',
value: 'Permission denied : '+ type
})
})
// Set own user and root channel
this.thisUser(client.self.__ui)
this.root(client.root.__ui)
@ -772,6 +803,26 @@ class GlobalBindings {
this.requestMove = (user, channel) => {
if (this.connected()) {
user.model.setChannel(channel.model)
let currentUrl = url.parse(document.location.href, true)
// delete search param so that query one can be taken into account
delete currentUrl.search
// get full channel path
if( channel.parent() ){ // in case this channel is not Root
let parent = channel.parent()
currentUrl.query.channelName = channel.name()
while( parent.parent() ){
currentUrl.query.channelName = parent.name() + '/' + currentUrl.query.channelName
parent = parent.parent()
}
} else {
// there is no channelName as we moved to Root
delete currentUrl.query.channelName
}
// reflect this change in URL
window.history.pushState(null, channel.name(), url.format(currentUrl))
}
}
@ -883,7 +934,7 @@ var ui = new GlobalBindings(window.mumbleWebConfig)
// Used only for debugging
window.mumbleUi = ui
window.onload = function () {
function initializeUI () {
var queryParams = url.parse(document.location.href, true).query
queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
var useJoinDialog = queryParams.joinDialog
@ -901,7 +952,11 @@ window.onload = function () {
useJoinDialog = false
}
if (queryParams.token) {
ui.connectDialog.token(queryParams.token)
var tokens = queryParams.token
if (!Array.isArray(tokens)) {
tokens = [tokens]
}
ui.connectDialog.tokens(tokens)
}
if (queryParams.username) {
ui.connectDialog.username(queryParams.username)
@ -911,6 +966,9 @@ window.onload = function () {
if (queryParams.password) {
ui.connectDialog.password(queryParams.password)
}
if (queryParams.channelName) {
ui.connectDialog.channelName(queryParams.channelName)
}
if (queryParams.avatarurl) {
// Download the avatar and upload it to the mumble server when connected
let url = queryParams.avatarurl
@ -939,13 +997,11 @@ window.onload = function () {
req.send()
}
ui.connectDialog.joinOnly(useJoinDialog)
userMediaPromise.then(() => {
ko.applyBindings(ui)
})
}
ko.applyBindings(ui)
window.onresize = () => ui.updateSize()
ui.updateSize()
window.onresize = () => ui.updateSize()
ui.updateSize()
}
function log () {
console.log.apply(console, arguments)
@ -996,20 +1052,111 @@ function userToState () {
var voiceHandler
var testVoiceHandler
var userMediaPromise = initVoice(data => {
if (testVoiceHandler) {
testVoiceHandler.write(data)
}
if (!ui.client) {
if (voiceHandler) {
voiceHandler.end()
/**
* @author svartoyg
*/
function translatePiece(selector, kind, parameters, key) {
let element = document.querySelector(selector);
if (element !== null) {
const translation = translate(key);
switch (kind) {
default:
console.warn('unhandled dom translation kind "' + kind + '"');
break;
case 'textcontent':
element.textContent = translation;
break;
case 'attribute':
element.setAttribute(parameters.name || 'value', translation);
break;
}
voiceHandler = null
} else if (voiceHandler) {
voiceHandler.write(data)
} else {
console.warn(`translation selector "${selector}" for "${key}" did not match any element`)
}
}).then(userMedia => {
ui._micStream = userMedia
}, err => {
window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
})
}
/**
* @author svartoyg
*/
function translateEverything() {
translatePiece('#connect-dialog_title', 'textcontent', {}, 'connectdialog.title');
translatePiece('#connect-dialog_input_address', 'textcontent', {}, 'connectdialog.address');
translatePiece('#connect-dialog_input_port', 'textcontent', {}, 'connectdialog.port');
translatePiece('#connect-dialog_input_username', 'textcontent', {}, 'connectdialog.username');
translatePiece('#connect-dialog_input_password', 'textcontent', {}, 'connectdialog.password');
translatePiece('#connect-dialog_input_tokens', 'textcontent', {}, 'connectdialog.tokens');
translatePiece('#connect-dialog_controls_remove', 'textcontent', {}, 'connectdialog.remove');
translatePiece('#connect-dialog_controls_add', 'textcontent', {}, 'connectdialog.add');
translatePiece('#connect-dialog_controls_cancel', 'attribute', {'name': 'value'}, 'connectdialog.cancel');
translatePiece('#connect-dialog_controls_connect', 'attribute', {'name': 'value'}, 'connectdialog.connect');
translatePiece('.connect-dialog.error-dialog .dialog-header', 'textcontent', {}, 'connectdialog.error.title');
translatePiece('.connect-dialog.error-dialog .reason .refused', 'textcontent', {}, 'connectdialog.error.reason.refused');
translatePiece('.connect-dialog.error-dialog .reason .version', 'textcontent', {}, 'connectdialog.error.reason.version');
translatePiece('.connect-dialog.error-dialog .reason .username', 'textcontent', {}, 'connectdialog.error.reason.username');
translatePiece('.connect-dialog.error-dialog .reason .userpassword', 'textcontent', {}, 'connectdialog.error.reason.userpassword');
translatePiece('.connect-dialog.error-dialog .reason .serverpassword', 'textcontent', {}, 'connectdialog.error.reason.serverpassword');
translatePiece('.connect-dialog.error-dialog .reason .username-in-use', 'textcontent', {}, 'connectdialog.error.reason.username_in_use');
translatePiece('.connect-dialog.error-dialog .reason .full', 'textcontent', {}, 'connectdialog.error.reason.full');
translatePiece('.connect-dialog.error-dialog .reason .clientcert', 'textcontent', {}, 'connectdialog.error.reason.clientcert');
translatePiece('.connect-dialog.error-dialog .reason .server', 'textcontent', {}, 'connectdialog.error.reason.server');
translatePiece('.connect-dialog.error-dialog .alternate-username', 'textcontent', {}, 'connectdialog.username');
translatePiece('.connect-dialog.error-dialog .alternate-password', 'textcontent', {}, 'connectdialog.password');
translatePiece('.connect-dialog.error-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'connectdialog.error.retry');
translatePiece('.connect-dialog.error-dialog .dialog-close', 'attribute', {'name': 'value'}, 'connectdialog.error.cancel');
translatePiece('.join-dialog .dialog-header', 'textcontent', {}, 'joindialog.title');
translatePiece('.join-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'joindialog.connect');
translatePiece('.user-context-menu .mute', 'textcontent', {}, 'contextmenu.mute');
translatePiece('.user-context-menu .deafen', 'textcontent', {}, 'contextmenu.deafen');
translatePiece('.user-context-menu .priority-speaker', 'textcontent', {}, 'usercontextmenu.priority_speaker');
translatePiece('.user-context-menu .local-mute', 'textcontent', {}, 'usercontextmenu.local_mute');
translatePiece('.user-context-menu .ignore-messages', 'textcontent', {}, 'usercontextmenu.ignore_messages');
translatePiece('.user-context-menu .view-comment', 'textcontent', {}, 'usercontextmenu.view_comment');
translatePiece('.user-context-menu .change-comment', 'textcontent', {}, 'usercontextmenu.change_comment');
translatePiece('.user-context-menu .reset-comment', 'textcontent', {}, 'usercontextmenu.reset_comment');
translatePiece('.user-context-menu .view-avatar', 'textcontent', {}, 'usercontextmenu.view_avatar');
translatePiece('.user-context-menu .change-avatar', 'textcontent', {}, 'usercontextmenu.change_avatar');
translatePiece('.user-context-menu .reset-avatar', 'textcontent', {}, 'usercontextmenu.reset_avatar');
translatePiece('.user-context-menu .send-message', 'textcontent', {}, 'usercontextmenu.send_message');
translatePiece('.user-context-menu .information', 'textcontent', {}, 'usercontextmenu.information');
translatePiece('.user-context-menu .self-mute', 'textcontent', {}, 'usercontextmenu.self_mute');
translatePiece('.user-context-menu .self-deafen', 'textcontent', {}, 'usercontextmenu.self_deafen');
translatePiece('.user-context-menu .add-friend', 'textcontent', {}, 'usercontextmenu.add_friend');
translatePiece('.user-context-menu .remove-friend', 'textcontent', {}, 'usercontextmenu.remove_friend');
translatePiece('.channel-context-menu .join', 'textcontent', {}, 'channelcontextmenu.join');
translatePiece('.channel-context-menu .add', 'textcontent', {}, 'channelcontextmenu.add');
translatePiece('.channel-context-menu .edit', 'textcontent', {}, 'channelcontextmenu.edit');
translatePiece('.channel-context-menu .remove', 'textcontent', {}, 'channelcontextmenu.remove');
translatePiece('.channel-context-menu .link', 'textcontent', {}, 'channelcontextmenu.link');
translatePiece('.channel-context-menu .unlink', 'textcontent', {}, 'channelcontextmenu.unlink');
translatePiece('.channel-context-menu .unlink-all', 'textcontent', {}, 'channelcontextmenu.unlink_all');
translatePiece('.channel-context-menu .copy-mumble-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_url');
translatePiece('.channel-context-menu .copy-mumble-web-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_web_url');
translatePiece('.channel-context-menu .send-message', 'textcontent', {}, 'channelcontextmenu.send_message');
}
async function main() {
await localizationInitialize(navigator.language);
translateEverything();
try {
const userMedia = await initVoice(data => {
if (testVoiceHandler) {
testVoiceHandler.write(data)
}
if (!ui.client) {
if (voiceHandler) {
voiceHandler.end()
}
voiceHandler = null
} else if (voiceHandler) {
voiceHandler.write(data)
}
})
ui._micStream = userMedia
} catch (err) {
window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
return
}
initializeUI();
}
window.onload = main

99
app/loc.js Normal file
View file

@ -0,0 +1,99 @@
/**
* the default language to use
*
* @var {string}
* @author svartoyg
*/
var _languageDefault = null;
/**
* the fallback language to use
*
* @var {string}
* @author svartoyg
*/
var _languageFallback = null;
/**
* two level map with ISO-639-1 code as first key and translation id as second key
*
* @var {Map<string,Map<string,string>>}
* @author svartoyg
*/
var _data = {};
/**
* @param {string} language
* @return Promise<Map<string,string>>
* @author svartoyg
*/
async function retrieveData (language) {
let json
try {
json = (await import(`../loc/${language}.json`)).default
} catch (exception) {
json = (await import(`../loc/${language.substr(0, language.indexOf('-'))}.json`)).default
}
const map = {}
flatten(json, '', map)
return map
}
function flatten (tree, prefix, result) {
for (const [key, value] of Object.entries(tree)) {
if (typeof value === 'string') {
result[prefix + key] = value
} else {
flatten(value, prefix + key + '.', result)
}
}
}
/**
* @param {string} languageDefault
* @param {string} [languageFallback]
* @author svartoyg
*/
export async function initialize (languageDefault, languageFallback = 'en') {
_languageFallback = languageFallback;
_languageDefault = languageDefault;
for (const language of [_languageFallback, _languageDefault]) {
if (_data.hasOwnProperty(language)) continue;
console.log('--', 'loading localization data for language "' + language + '" ...');
let data;
try {
data = await retrieveData(language);
} catch (exception) {
console.warn(exception.toString());
}
_data[language] = data;
}
}
/**
* gets a translation by its key for a specific language
*
* @param {string} key
* @param {string} [languageChosen]
* @return {string}
* @author svartoyg
*/
export function translate (key, languageChosen = _languageDefault) {
let result = undefined;
for (const language of [languageChosen, _languageFallback]) {
if (_data.hasOwnProperty(language) && (_data[language] !== undefined) && _data[language].hasOwnProperty(key)) {
result = _data[language][key];
break;
}
}
if (result === undefined) {
result = ('{{' + key + '}}');
}
return result;
}

View file

@ -4,8 +4,7 @@ import EventEmitter from 'events'
import { Writable, PassThrough } from 'stream'
import toArrayBuffer from 'to-arraybuffer'
import ByteBuffer from 'bytebuffer'
import webworkify from 'webworkify'
import worker from './worker'
import Worker from './worker'
/**
* Creates proxy MumbleClients to a real ones running on a web worker.
@ -13,7 +12,7 @@ import worker from './worker'
*/
class WorkerBasedMumbleConnector {
constructor () {
this._worker = webworkify(worker)
this._worker = new Worker()
this._worker.addEventListener('message', this._onMessage.bind(this))
this._reqId = 1
this._requests = {}
@ -266,7 +265,7 @@ class WorkerBasedMumbleChannel extends EventEmitter {
_dispatchEvent (name, args) {
if (name === 'update') {
let [actor, props] = args
let [props] = args
Object.entries(props).forEach((entry) => {
this._setProp(entry[0], entry[1])
})
@ -277,7 +276,6 @@ class WorkerBasedMumbleChannel extends EventEmitter {
props.links = this.links
}
args = [
this._client._user(actor),
props
]
} else if (name === 'remove') {

View file

@ -3,18 +3,11 @@ import mumbleConnect from 'mumble-client-websocket'
import toArrayBuffer from 'to-arraybuffer'
import chunker from 'stream-chunker'
import Resampler from 'libsamplerate.js'
import CodecsBrowser from 'mumble-client-codecs-browser'
// Polyfill nested webworkers for https://bugs.chromium.org/p/chromium/issues/detail?id=31666
import 'subworkers'
// Monkey-patch to allow webworkify-webpack and codecs to work inside of web worker
/* global URL */
window.URL = URL
// Using require to ensure ordering relative to monkey-patch above
let CodecsBrowser = require('mumble-client-codecs-browser')
export default function (self) {
let sampleRate
let nextClientId = 1
let nextVoiceId = 1
@ -97,17 +90,14 @@ export default function (self) {
function setupChannel (id, channel) {
id = Object.assign({}, id, { channel: channel.id })
registerEventProxy(id, channel, 'update', (actor, props) => {
if (actor) {
actor = actor.id
}
registerEventProxy(id, channel, 'update', (props) => {
if (props.parent) {
props.parent = props.parent.id
}
if (props.links) {
props.links = props.links.map((it) => it.id)
}
return [actor, props]
return [props]
})
registerEventProxy(id, channel, 'remove')
@ -194,6 +184,7 @@ export default function (self) {
id = { client: id }
registerEventProxy(id, client, 'error')
registerEventProxy(id, client, 'denied', it => [it])
registerEventProxy(id, client, 'newChannel', (it) => [setupChannel(id, it)])
registerEventProxy(id, client, 'newUser', (it) => [setupUser(id, it)])
registerEventProxy(id, client, 'message', (sender, message, users, channels, trees) => {
@ -284,4 +275,5 @@ export default function (self) {
console.error('exception during message event', ev.data, ex)
}
})
}
export default null