Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Adam <dev@robert-adam.de>2021-04-16 21:51:23 +0300
committerGitHub <noreply@github.com>2021-04-16 21:51:23 +0300
commitd4a5fc1047837ce2018a6381e0e74a51f65b2a8f (patch)
treef45a8df3f53649df21f0e5094d7021bae6f238fa /src/mumble
parentae3cc1ea692b8377dacd85e8dab71cff8827a5b6 (diff)
parent5db2400af55776b5034775c36c1cae033a478091 (diff)
Merge PR #3743: FEAT(client): Plugin framework
This commit introduces a new plugin framework into the codebase of the Mumble client. Note that "plugin" here really refers to a (more or less) general purpose plugin and is therefore not to be confused with the previously available positional data plugins (only responsible for fetching positional data from a running game and passing that to Mumble). The plugin interface is written in C, removing the compiler-dependence the old "plugins" had. Instead plugins can now be written in an arbitrary language as long as that language is capable of being compiled into a shared library and also being capable of being C-compatible. As already indicated a plugin is essentially a shared library that provides certain functions that allow Mumble to interface with it. Inside Mumble the so-called PluginManager is responsible for managing the plugins and relaying events to the respective callbacks. Plugins themselves can also interact with Mumble on their own initiative by using the provided API functions. Fixes #2455 Fixes #2148 Fixes #1594 Fixes #2051 Fixes #3742 Fixes #4575 Fixes #4751
Diffstat (limited to 'src/mumble')
-rw-r--r--src/mumble/API.h188
-rw-r--r--src/mumble/API_v_1_0_x.cpp1980
-rw-r--r--src/mumble/Audio.cpp10
-rw-r--r--src/mumble/AudioInput.cpp21
-rw-r--r--src/mumble/AudioInput.h8
-rw-r--r--src/mumble/AudioOutput.cpp145
-rw-r--r--src/mumble/AudioOutput.h19
-rw-r--r--src/mumble/CMakeLists.txt107
-rw-r--r--src/mumble/ClientUser.cpp4
-rw-r--r--src/mumble/Global.cpp2
-rw-r--r--src/mumble/Global.h5
-rw-r--r--src/mumble/LegacyPlugin.cpp267
-rw-r--r--src/mumble/LegacyPlugin.h82
-rw-r--r--src/mumble/Log.cpp8
-rw-r--r--src/mumble/Log.h10
-rw-r--r--src/mumble/MainWindow.cpp37
-rw-r--r--src/mumble/MainWindow.h8
-rw-r--r--src/mumble/ManualPlugin.cpp39
-rw-r--r--src/mumble/ManualPlugin.h23
-rw-r--r--src/mumble/Messages.cpp21
-rw-r--r--src/mumble/NetworkConfig.cpp11
-rw-r--r--src/mumble/NetworkConfig.ui13
-rw-r--r--src/mumble/Plugin.cpp694
-rw-r--r--src/mumble/Plugin.h417
-rw-r--r--src/mumble/PluginConfig.cpp247
-rw-r--r--src/mumble/PluginConfig.h66
-rw-r--r--src/mumble/PluginConfig.ui (renamed from src/mumble/Plugins.ui)41
-rw-r--r--src/mumble/PluginInstaller.cpp200
-rw-r--r--src/mumble/PluginInstaller.h84
-rw-r--r--src/mumble/PluginInstaller.ui243
-rw-r--r--src/mumble/PluginManager.cpp933
-rw-r--r--src/mumble/PluginManager.h279
-rw-r--r--src/mumble/PluginUpdater.cpp379
-rw-r--r--src/mumble/PluginUpdater.h107
-rw-r--r--src/mumble/PluginUpdater.ui224
-rw-r--r--src/mumble/Plugins.cpp792
-rw-r--r--src/mumble/Plugins.h91
-rw-r--r--src/mumble/PositionalData.cpp242
-rw-r--r--src/mumble/PositionalData.h171
-rw-r--r--src/mumble/ServerHandler.cpp18
-rw-r--r--src/mumble/ServerHandler.h10
-rw-r--r--src/mumble/Settings.cpp86
-rw-r--r--src/mumble/Settings.h16
-rw-r--r--src/mumble/UserModel.cpp11
-rw-r--r--src/mumble/UserModel.h21
-rw-r--r--src/mumble/main.cpp97
-rw-r--r--src/mumble/mumble_ar.ts191
-rw-r--r--src/mumble/mumble_bg.ts193
-rw-r--r--src/mumble/mumble_br.ts191
-rw-r--r--src/mumble/mumble_ca.ts191
-rw-r--r--src/mumble/mumble_cs.ts203
-rw-r--r--src/mumble/mumble_cy.ts193
-rw-r--r--src/mumble/mumble_da.ts203
-rw-r--r--src/mumble/mumble_de.ts203
-rw-r--r--src/mumble/mumble_el.ts203
-rw-r--r--src/mumble/mumble_en.ts191
-rw-r--r--src/mumble/mumble_en_GB.ts191
-rw-r--r--src/mumble/mumble_eo.ts193
-rw-r--r--src/mumble/mumble_es.ts203
-rw-r--r--src/mumble/mumble_et.ts195
-rw-r--r--src/mumble/mumble_eu.ts201
-rw-r--r--src/mumble/mumble_fa_IR.ts191
-rw-r--r--src/mumble/mumble_fi.ts203
-rw-r--r--src/mumble/mumble_fr.ts203
-rw-r--r--src/mumble/mumble_gl.ts191
-rw-r--r--src/mumble/mumble_he.ts204
-rw-r--r--src/mumble/mumble_hu.ts203
-rw-r--r--src/mumble/mumble_it.ts257
-rw-r--r--src/mumble/mumble_ja.ts203
-rw-r--r--src/mumble/mumble_ko.ts203
-rw-r--r--src/mumble/mumble_lt.ts199
-rw-r--r--src/mumble/mumble_nl.ts258
-rw-r--r--src/mumble/mumble_no.ts203
-rw-r--r--src/mumble/mumble_oc.ts191
-rw-r--r--src/mumble/mumble_pl.ts259
-rw-r--r--src/mumble/mumble_pt_BR.ts203
-rw-r--r--src/mumble/mumble_pt_PT.ts203
-rw-r--r--src/mumble/mumble_ro.ts191
-rw-r--r--src/mumble/mumble_ru.ts203
-rw-r--r--src/mumble/mumble_si.ts197
-rw-r--r--src/mumble/mumble_sv.ts259
-rw-r--r--src/mumble/mumble_te.ts191
-rw-r--r--src/mumble/mumble_th.ts191
-rw-r--r--src/mumble/mumble_tr.ts203
-rw-r--r--src/mumble/mumble_uk.ts191
-rw-r--r--src/mumble/mumble_zh_CN.ts252
-rw-r--r--src/mumble/mumble_zh_HK.ts203
-rw-r--r--src/mumble/mumble_zh_TW.ts203
88 files changed, 15110 insertions, 1970 deletions
diff --git a/src/mumble/API.h b/src/mumble/API.h
new file mode 100644
index 000000000..74d8196ae
--- /dev/null
+++ b/src/mumble/API.h
@@ -0,0 +1,188 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_API_H_
+#define MUMBLE_MUMBLE_API_H_
+
+// In here the MumbleAPI struct is defined
+#include "MumbleAPI_v_1_0_x.h"
+
+#include <atomic>
+#include <functional>
+#include <future>
+#include <unordered_map>
+
+#include <QObject>
+
+namespace API {
+
+using api_future_t = std::future< mumble_error_t >;
+using api_promise_t = std::promise< mumble_error_t >;
+
+/// A "curator" that will keep track of allocated resources and how to delete them
+struct MumbleAPICurator {
+ struct Entry {
+ /// The function used to delete the corresponding pointer
+ std::function< void(const void *) > m_deleter;
+ /// The ID of the plugin the resource pointed at was allocated for
+ mumble_plugin_id_t m_pluginID;
+ /// The name of the API function the resource pointed to was allocated in
+ /// NOTE: This must only ever be a pointer to a String literal.
+ const char *m_sourceFunctionName;
+ };
+
+ std::unordered_map< const void *, Entry > m_entries;
+
+ ~MumbleAPICurator();
+};
+
+/// This object contains the actual API implementation. It also takes care of synchronizing API calls
+/// with Mumble's main thread so that plugins can call them from an arbitrary thread without causing
+/// issues.
+/// This class is a singleton as a way to be able to write C function wrappers for the member functions
+/// that are needed for passing to the plugins.
+class MumbleAPI : public QObject {
+ Q_OBJECT;
+ Q_DISABLE_COPY(MumbleAPI);
+
+public:
+ static MumbleAPI &get();
+
+public slots:
+ // The description of the functions is provided in MumbleAPI.h
+
+ // Note that every slot is synchronized and is therefore guaranteed to be executed in the main
+ // thread. For the synchronization strategy see below.
+ void freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise);
+ void getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection,
+ api_promise_t *promise);
+ void isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ bool *synchronized, api_promise_t *promise);
+ void getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t *userID,
+ api_promise_t *promise);
+ void getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **name, api_promise_t *promise);
+ void getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name, api_promise_t *promise);
+ void getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t **users,
+ size_t *userCount, api_promise_t *promise);
+ void getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise);
+ void getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t *channelID, api_promise_t *promise);
+ void getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount,
+ api_promise_t *promise);
+ void getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode,
+ api_promise_t *promise);
+ void isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ bool *muted, api_promise_t *promise);
+ void isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise);
+ void isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise);
+ void getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash, api_promise_t *promise);
+ void getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash,
+ api_promise_t *promise);
+ void requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t transmissionMode, api_promise_t *promise);
+ void getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **comment, api_promise_t *promise);
+ void getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **description, api_promise_t *promise);
+ void requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t channelID, const char *password, api_promise_t *promise);
+ void requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate,
+ api_promise_t *promise);
+ void requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ bool muted, api_promise_t *promise);
+ void requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise);
+ void requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise);
+ void requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *comment, api_promise_t *promise);
+ void findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *userName,
+ mumble_userid_t *userID, api_promise_t *promise);
+ void findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *channelName,
+ mumble_channelid_t *channelID, api_promise_t *promise);
+ void getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char **outValue,
+ api_promise_t *promise);
+ void setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value,
+ api_promise_t *promise);
+ void setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value,
+ api_promise_t *promise);
+ void setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value,
+ api_promise_t *promise);
+ void setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char *value,
+ api_promise_t *promise);
+ void sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const mumble_userid_t *users,
+ size_t userCount, const uint8_t *data, size_t dataLength, const char *dataID,
+ api_promise_t *promise);
+ void log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise);
+ void playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise);
+
+
+private:
+ MumbleAPI();
+
+ MumbleAPICurator m_curator;
+};
+
+/// @returns The Mumble API struct (v1.0.x)
+MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x();
+
+/// Converts from the Qt key-encoding to the API's key encoding.
+///
+/// @param keyCode The Qt key-code that shall be converted
+/// @returns The converted key code or KC_INVALID if conversion failed
+mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode);
+
+/// A class holding non-permanent data set by plugins. Non-permanent means that this data
+/// will not be stored between restarts.
+/// All member field should be atomic in order to be thread-safe
+class PluginData {
+public:
+ /// Constructor
+ PluginData();
+ /// Destructor
+ ~PluginData();
+
+ /// A flag indicating whether a plugin has requested the microphone to be permanently on (mirroring the
+ /// behaviour of the continous transmission mode.
+ std::atomic_bool overwriteMicrophoneActivation;
+
+ /// @returns A reference to the PluginData singleton
+ static PluginData &get();
+}; // class PluginData
+}; // namespace API
+
+
+// Declare the meta-types that we require in order for the API to work
+Q_DECLARE_METATYPE(mumble_settings_key_t);
+Q_DECLARE_METATYPE(mumble_settings_key_t *);
+Q_DECLARE_METATYPE(mumble_transmission_mode_t);
+Q_DECLARE_METATYPE(mumble_transmission_mode_t *);
+Q_DECLARE_METATYPE(API::api_promise_t *);
+
+//////////////////////////////////////////////////////////////
+///////////// SYNCHRONIZATION STRATEGY ///////////////////////
+//////////////////////////////////////////////////////////////
+
+/**
+ * Every API function call checks whether it is being called from the main thread. If it is,
+ * it continues executing as usual. If it is not however, it uses Qt's signal/slot mechanism
+ * to schedule the respective function to be run in the main thread in the next iteration of
+ * the event loop.
+ * In order to synchronize with the calling thread, the return value (error code) of these
+ * functions is "returned" as a promise. Thus by accessing the exit code via the corresponding
+ * future, the calling thread is blocked until the function has been executed in the main thread
+ * (and thereby set the exit code once it is done allowing the calling thread to unblock).
+ */
+
+#endif
diff --git a/src/mumble/API_v_1_0_x.cpp b/src/mumble/API_v_1_0_x.cpp
new file mode 100644
index 000000000..ac8adf6f1
--- /dev/null
+++ b/src/mumble/API_v_1_0_x.cpp
@@ -0,0 +1,1980 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "API.h"
+#include "AudioOutput.h"
+#include "Channel.h"
+#include "ClientUser.h"
+#include "Database.h"
+#include "Log.h"
+#include "MainWindow.h"
+#include "PluginComponents_v_1_0_x.h"
+#include "PluginManager.h"
+#include "ServerHandler.h"
+#include "Settings.h"
+#include "UserModel.h"
+#include "MumbleConstants.h"
+#include "Global.h"
+
+#include <QVariant>
+#include <QtCore/QHash>
+#include <QtCore/QMutex>
+#include <QtCore/QMutexLocker>
+#include <QtCore/QReadLocker>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+
+#include <cstring>
+#include <string>
+
+#define EXIT_WITH(code) \
+ if (promise) { \
+ promise->set_value(code); \
+ } \
+ return;
+
+#define VERIFY_PLUGIN_ID(id) \
+ if (!Global::get().pluginManager->pluginExists(id)) { \
+ EXIT_WITH(EC_INVALID_PLUGIN_ID); \
+ }
+
+// Right now there can only be one connection managed by the current ServerHandler
+#define VERIFY_CONNECTION(connection) \
+ if (!Global::get().sh || Global::get().sh->getConnectionID() != connection) { \
+ EXIT_WITH(EC_CONNECTION_NOT_FOUND); \
+ }
+
+// Right now whether or not a connection has finished synchronizing is indicated by Global::get().uiSession. If it is zero,
+// synchronization is not done yet (or there is no connection to begin with). The connection parameter in the macro is
+// only present in case it will be needed in the future
+#define ENSURE_CONNECTION_SYNCHRONIZED(connection) \
+ if (Global::get().uiSession == 0) { \
+ EXIT_WITH(EC_CONNECTION_UNSYNCHRONIZED); \
+ }
+
+#define UNUSED(var) (void) var;
+
+namespace API {
+
+MumbleAPICurator::~MumbleAPICurator() {
+ // free all remaining resources using the stored deleters
+ for (const auto &current : m_entries) {
+ const Entry &entry = current.second;
+
+ // Delete leaked resource
+ entry.m_deleter(current.first);
+
+ // Print an error about the leaked resource
+ printf("[ERROR]: Plugin with ID %d leaked memory from a call to API function \"%s\"\n", entry.m_pluginID, entry.m_sourceFunctionName);
+ }
+}
+// Some common delete-functions
+void defaultDeleter(const void *ptr) {
+ // We use const-cast in order to circumvent the shortcoming of the free() signature only taking
+ // in void * and not const void *. Delete on the other hand is allowed on const pointers which is
+ // why this is an okay thing to do.
+ // See also https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c
+ free(const_cast< void * >(ptr));
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////// API IMPLEMENTATION //////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// This macro registers type, type * and type ** to Qt's metatype system
+// and also their const variants (except const value as that doesn't make sense)
+#define REGISTER_METATYPE(type) \
+ qRegisterMetaType< type >(#type); \
+ qRegisterMetaType< type * >(#type " *"); \
+ qRegisterMetaType< type ** >(#type " **"); \
+ qRegisterMetaType< const type * >("const " #type " *"); \
+ qRegisterMetaType< const type ** >("const " #type " **"); \
+
+MumbleAPI::MumbleAPI() {
+ // Move this object to the main thread
+ moveToThread(qApp->thread());
+
+ // Register all API types to Qt's metatype system
+ REGISTER_METATYPE(bool);
+ REGISTER_METATYPE(char);
+ REGISTER_METATYPE(double);
+ REGISTER_METATYPE(int);
+ REGISTER_METATYPE(int64_t);
+ REGISTER_METATYPE(mumble_channelid_t);
+ REGISTER_METATYPE(mumble_connection_t);
+ REGISTER_METATYPE(mumble_plugin_id_t);
+ REGISTER_METATYPE(mumble_settings_key_t);
+ REGISTER_METATYPE(mumble_transmission_mode_t);
+ REGISTER_METATYPE(mumble_userid_t);
+ REGISTER_METATYPE(mumble_userid_t);
+ REGISTER_METATYPE(size_t);
+ REGISTER_METATYPE(uint8_t);
+
+ // Define additional types that can't be defined using macro REGISTER_METATYPE
+ qRegisterMetaType< api_promise_t * >("api_promise_t *");
+ qRegisterMetaType< API::api_promise_t * >("API::api_promise_t *");
+ qRegisterMetaType< const void * >("const void *");
+ qRegisterMetaType< const void ** >("const void **");
+ qRegisterMetaType< void * >("void *");
+ qRegisterMetaType< void ** >("void **");
+}
+
+#undef REFGISTER_METATYPE
+
+MumbleAPI &MumbleAPI::get() {
+ static MumbleAPI api;
+
+ return api;
+}
+
+void MumbleAPI::freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "freeMemory_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const void *, ptr), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ // Don't verify plugin ID here to avoid memory leaks
+ UNUSED(callerID);
+
+ auto it = m_curator.m_entries.find(ptr);
+ if (it != m_curator.m_entries.cend()) {
+ MumbleAPICurator::Entry &entry = (*it).second;
+
+ // call the deleter to delete the resource
+ entry.m_deleter(ptr);
+
+ // Remove pointer from curator
+ m_curator.m_entries.erase(it);
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_POINTER_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getActiveServerConnection_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t *, connection),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (Global::get().sh) {
+ *connection = Global::get().sh->getConnectionID();
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_NO_ACTIVE_CONNECTION);
+ }
+}
+
+void MumbleAPI::isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ bool *synchronized, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isConnectionSynchronized_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(bool *, synchronized), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+ VERIFY_CONNECTION(connection);
+
+ // Right now there can only be one connection and if Global::get().uiSession is zero, then the synchronization has not finished
+ // yet (or there is no connection to begin with)
+ *synchronized = Global::get().uiSession != 0;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t *userID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getLocalUserID_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t *, userID), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ *userID = Global::get().uiSession;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **name, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, name),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (user) {
+ // +1 for NULL terminator
+ size_t size = user->qsName.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, user->qsName.toUtf8().data());
+
+ // save the allocated pointer and how to delete it
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserName" } });
+
+ *name = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, name),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (channel) {
+ // +1 for NULL terminator
+ size_t size = channel->qsName.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, channel->qsName.toUtf8().data());
+
+ // save the allocated pointer and how to delete it
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelName" } });
+
+ *name = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t **users, size_t *userCount, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getAllUsers_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t **, users), Q_ARG(size_t *, userCount),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ QReadLocker userLock(&ClientUser::c_qrwlUsers);
+
+ size_t amount = ClientUser::c_qmUsers.size();
+
+ auto it = ClientUser::c_qmUsers.constBegin();
+
+ mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount));
+
+ unsigned int index = 0;
+ while (it != ClientUser::c_qmUsers.constEnd()) {
+ userIDs[index] = it.key();
+
+ it++;
+ index++;
+ }
+
+ m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getAllUsers" } });
+
+ *users = userIDs;
+ *userCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getAllChannels_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t **, channels), Q_ARG(size_t *, channelCount),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ QReadLocker channelLock(&Channel::c_qrwlChannels);
+
+ size_t amount = Channel::c_qhChannels.size();
+
+ auto it = Channel::c_qhChannels.constBegin();
+
+ mumble_channelid_t *channelIDs =
+ reinterpret_cast< mumble_channelid_t * >(malloc(sizeof(mumble_channelid_t) * amount));
+
+ unsigned int index = 0;
+ while (it != Channel::c_qhChannels.constEnd()) {
+ channelIDs[index] = it.key();
+
+ it++;
+ index++;
+ }
+
+ m_curator.m_entries.insert({ channelIDs, { defaultDeleter, callerID, "getAllChannels" } });
+
+ *channels = channelIDs;
+ *channelCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t *channelID,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelOfUser_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t *, channelID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (user->cChannel) {
+ *channelID = user->cChannel->iId;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_GENERIC_ERROR);
+ }
+}
+
+void MumbleAPI::getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUsersInChannel_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(mumble_userid_t **, users),
+ Q_ARG(size_t *, userCount), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ size_t amount = channel->qlUsers.size();
+
+ mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount));
+
+ int index = 0;
+ foreach (const User *currentUser, channel->qlUsers) {
+ userIDs[index] = currentUser->uiSession;
+
+ index++;
+ }
+
+ m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getUsersInChannel" } });
+
+ *users = userIDs;
+ *userCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t *transmissionMode,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(
+ this, "getLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_transmission_mode_t *, transmissionMode), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ switch (Global::get().s.atTransmit) {
+ case Settings::AudioTransmit::Continuous:
+ *transmissionMode = TM_CONTINOUS;
+ EXIT_WITH(STATUS_OK);
+ case Settings::AudioTransmit::VAD:
+ *transmissionMode = TM_VOICE_ACTIVATION;
+ EXIT_WITH(STATUS_OK);
+ case Settings::AudioTransmit::PushToTalk:
+ *transmissionMode = TM_PUSH_TO_TALK;
+ EXIT_WITH(STATUS_OK);
+ }
+
+ // Unable to resolve transmission mode
+ EXIT_WITH(EC_GENERIC_ERROR);
+}
+
+void MumbleAPI::isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool *muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isUserLocallyMuted_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(bool *, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ *muted = user->bLocalMute;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isLocalUserMuted_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ *muted = Global::get().s.bMute;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isLocalUserDeafened_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, deafened),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ *deafened = Global::get().s.bDeaf;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserHash_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, hash),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ // The user's hash is already in hexadecimal representation, so we don't have to worry about null-bytes in it
+ // +1 for NULL terminator
+ size_t size = user->qsHash.toUtf8().size() + 1;
+
+ char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(hashArray, user->qsHash.toUtf8().data());
+
+ m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getUserHash" } });
+
+ *hash = hashArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getServerHash_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char **, hash), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ // Use hexadecimal representation in order for the String to be properly printable and for it to be C-encodable
+ QByteArray hashHex = Global::get().sh->qbaDigest.toHex();
+ QString strHash = QString::fromLatin1(hashHex);
+
+ // +1 for NULL terminator
+ size_t size = strHash.toUtf8().size() + 1;
+
+ char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(hashArray, strHash.toUtf8().data());
+
+ m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getServerHash" } });
+
+ *hash = hashArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t transmissionMode,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_transmission_mode_t, transmissionMode), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ switch (transmissionMode) {
+ case TM_CONTINOUS:
+ Global::get().s.atTransmit = Settings::AudioTransmit::Continuous;
+ EXIT_WITH(STATUS_OK);
+ case TM_VOICE_ACTIVATION:
+ Global::get().s.atTransmit = Settings::AudioTransmit::VAD;
+ EXIT_WITH(STATUS_OK);
+ case TM_PUSH_TO_TALK:
+ Global::get().s.atTransmit = Settings::AudioTransmit::PushToTalk;
+ EXIT_WITH(STATUS_OK);
+ }
+
+ EXIT_WITH(EC_UNKNOWN_TRANSMISSION_MODE);
+}
+
+void MumbleAPI::getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, const char **comment, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserComment_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, comment),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (user->qsComment.isEmpty() && !user->qbaCommentHash.isEmpty()) {
+ user->qsComment = QString::fromUtf8(Global::get().db->blob(user->qbaCommentHash));
+
+ if (user->qsComment.isEmpty()) {
+ // The user's comment hasn't been synchronized to this client yet
+ EXIT_WITH(EC_UNSYNCHRONIZED_BLOB);
+ }
+ }
+
+ // +1 for NULL terminator
+ size_t size = user->qsComment.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, user->qsComment.toUtf8().data());
+
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserComment" } });
+
+ *comment = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **description,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelDescription_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, description),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ if (channel->qsDesc.isEmpty() && !channel->qbaDescHash.isEmpty()) {
+ channel->qsDesc = QString::fromUtf8(Global::get().db->blob(channel->qbaDescHash));
+
+ if (channel->qsDesc.isEmpty()) {
+ // The channel's description hasn't been synchronized to this client yet
+ EXIT_WITH(EC_UNSYNCHRONIZED_BLOB);
+ }
+ }
+
+ // +1 for NULL terminator
+ size_t size = channel->qsDesc.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, channel->qsDesc.toUtf8().data());
+
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelDescription" } });
+
+ *description = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t channelID, const char *password,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestUserMove_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t, channelID),
+ Q_ARG(const char *, password), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ if (channel != user->cChannel) {
+ // send move-request to the server only if the user is not in the channel already
+ QStringList passwordList;
+ if (password) {
+ passwordList << QString::fromUtf8(password);
+ }
+
+ Global::get().sh->joinChannel(user->uiSession, channel->iId, passwordList);
+ }
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestMicrophoneActivationOverwrite_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, activate),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ PluginData::get().overwriteMicrophoneActivation.store(activate);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalMute_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(bool, muted), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ if (userID == Global::get().uiSession) {
+ // Can't locally mute the local user
+ EXIT_WITH(EC_INVALID_MUTE_TARGET);
+ }
+
+ ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ user->setLocalMute(muted);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserMute_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().mw) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->setAudioMute(muted);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserDeaf_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, deafened),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().mw) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->setAudioDeaf(deafened);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *comment, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestSetLocalUserComment_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, comment), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ ClientUser *localUser = ClientUser::get(Global::get().uiSession);
+
+ if (!localUser) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (!Global::get().mw || !Global::get().mw->pmModel) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->pmModel->setComment(localUser, QString::fromUtf8(comment));
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *userName, mumble_userid_t *userID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "findUserByName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, userName), Q_ARG(mumble_userid_t *, userID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const QString qsUserName = QString::fromUtf8(userName);
+
+ QReadLocker userLock(&ClientUser::c_qrwlUsers);
+
+ auto it = ClientUser::c_qmUsers.constBegin();
+ while (it != ClientUser::c_qmUsers.constEnd()) {
+ if (it.value()->qsName == qsUserName) {
+ *userID = it.key();
+
+ EXIT_WITH(STATUS_OK);
+ }
+
+ it++;
+ }
+
+ EXIT_WITH(EC_USER_NOT_FOUND);
+}
+
+void MumbleAPI::findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *channelName, mumble_channelid_t *channelID,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "findChannelByName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, channelName), Q_ARG(mumble_channelid_t *, channelID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const QString qsChannelName = QString::fromUtf8(channelName);
+
+ QReadLocker channelLock(&Channel::c_qrwlChannels);
+
+ auto it = Channel::c_qhChannels.constBegin();
+ while (it != Channel::c_qhChannels.constEnd()) {
+ if (it.value()->qsName == qsChannelName) {
+ *channelID = it.key();
+
+ EXIT_WITH(STATUS_OK);
+ }
+
+ it++;
+ }
+
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+}
+
+QVariant getMumbleSettingHelper(mumble_settings_key_t key) {
+ QVariant value;
+
+ // All values are explicitly cast to the target type of their associated API. For instance there is not API to
+ // get float values but there is one for doubles. Therefore floats have to be cast to doubles in order for the
+ // type checking to work out.
+ switch (key) {
+ case MSK_AUDIO_INPUT_VOICE_HOLD:
+ value = static_cast< int >(Global::get().s.iVoiceHold);
+ break;
+ case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD:
+ value = static_cast< double >(Global::get().s.fVADmin);
+ break;
+ case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD:
+ value = static_cast< double >(Global::get().s.fVADmax);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE:
+ value = static_cast< double >(Global::get().s.fAudioMinDistance);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE:
+ value = static_cast< double >(Global::get().s.fAudioMaxDistance);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_BLOOM:
+ value = static_cast< double >(Global::get().s.fAudioBloom);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME:
+ value = static_cast< double >(Global::get().s.fAudioMaxDistVolume);
+ break;
+ case MSK_INVALID:
+ // There is no setting associated with this key
+ break;
+ }
+
+ return value;
+}
+
+// IS_TYPE actually only checks if the QVariant can be converted to the needed type since that's all that we really care
+// about at the end of the day.
+#define IS_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) == varType
+#define IS_NOT_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) != varType
+
+void MumbleAPI::getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(bool *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Bool)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toBool();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_int_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(int64_t *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Int)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toInt();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ double *outValue, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_double_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(double *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Double)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toDouble();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ const char **outValue, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_string_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(const char **, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::QString)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ const QString stringValue = value.toString();
+
+ // +1 for NULL terminator
+ size_t size = stringValue.toUtf8().size() + 1;
+
+ char *valueArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(valueArray, stringValue.toUtf8().data());
+
+ m_curator.m_entries.insert({ valueArray, { defaultDeleter, callerID, "getMumbleSetting_string" } });
+
+ *outValue = valueArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+mumble_error_t setMumbleSettingHelper(mumble_settings_key_t key, QVariant value) {
+ switch (key) {
+ case MSK_AUDIO_INPUT_VOICE_HOLD:
+ if (IS_TYPE(value, QMetaType::Int)) {
+ Global::get().s.iVoiceHold = value.toInt();
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fVADmin = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fVADmax = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMinDistance = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMaxDistance = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_BLOOM:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioBloom = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMaxDistVolume = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_INVALID:
+ // Do nothing
+ break;
+ }
+
+ return EC_UNKNOWN_SETTINGS_KEY;
+}
+
+void MumbleAPI::setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(bool, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, value);
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_int_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(int64_t, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, QVariant::fromValue(value));
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_double_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(double, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, value);
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ const char *value, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_string_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(const char *, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, QString::fromUtf8(value));
+ EXIT_WITH(exitCode);
+}
+#undef IS_TYPE
+#undef IS_NOT_TYPE
+
+void MumbleAPI::sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const mumble_userid_t *users, size_t userCount, const uint8_t *data, size_t dataLength,
+ const char *dataID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "sendData_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_connection_t, connection), Q_ARG(const mumble_userid_t *, users),
+ Q_ARG(size_t, userCount), Q_ARG(const uint8_t *, data), Q_ARG(size_t, dataLength),
+ Q_ARG(const char *, dataID), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ if (dataLength > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) {
+ EXIT_WITH(EC_DATA_TOO_BIG);
+ }
+ if (std::strlen(dataID) > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) {
+ EXIT_WITH(EC_DATA_ID_TOO_LONG);
+ }
+
+ MumbleProto::PluginDataTransmission mpdt;
+ mpdt.set_sendersession(Global::get().uiSession);
+
+ for (size_t i = 0; i < userCount; i++) {
+ const ClientUser *user = ClientUser::get(users[i]);
+
+ if (user) {
+ mpdt.add_receiversessions(users[i]);
+ } else {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+ }
+
+ mpdt.set_data(data, dataLength);
+ mpdt.set_dataid(dataID);
+
+ if (Global::get().sh) {
+ Global::get().sh->sendMessage(mpdt);
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_CONNECTION_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "log_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const char *, message), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ // We verify the plugin manually as we need a handle to it later
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(callerID);
+ if (!plugin) {
+ EXIT_WITH(EC_INVALID_PLUGIN_ID);
+ }
+
+ QString msg = QString::fromLatin1("<b>%1:</b> %2")
+ .arg(plugin->getName().toHtmlEscaped())
+ .arg(QString::fromUtf8(message).toHtmlEscaped());
+
+ // Use static method that handles the case in which the Log object doesn't exist yet
+ Log::logOrDefer(Log::PluginMessage, msg);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "playSample_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const char *, samplePath), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().ao) {
+ EXIT_WITH(EC_AUDIO_NOT_AVAILABLE);
+ }
+
+ if (Global::get().ao->playSample(QString::fromUtf8(samplePath), false)) {
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_INVALID_SAMPLE);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////// C FUNCTION WRAPPERS FOR USE IN API STRUCT ///////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+mumble_error_t PLUGIN_CALLING_CONVENTION freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().freeMemory_v_1_0_x(callerID, ptr, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t *connection) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getActiveServerConnection_v_1_0_x(callerID, connection, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ bool *synchronized) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isConnectionSynchronized_v_1_0_x(callerID, connection, synchronized, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t *userID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getLocalUserID_v_1_0_x(callerID, connection, userID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **name) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserName_v_1_0_x(callerID, connection, userID, name, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelName_v_1_0_x(callerID, connection, channelID, name, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getAllUsers_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t **users,
+ size_t *userCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getAllUsers_v_1_0_x(callerID, connection, users, userCount, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getAllChannels_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getAllChannels_v_1_0_x(callerID, connection, channels, channelCount, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t *channel) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelOfUser_v_1_0_x(callerID, connection, userID, channel, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID,
+ mumble_userid_t **userList, size_t *userCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUsersInChannel_v_1_0_x(callerID, connection, channelID, userList, userCount, &promise);
+
+ return future.get();
+}
+
+
+mumble_error_t PLUGIN_CALLING_CONVENTION
+ getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, bool *muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isUserLocallyMuted_v_1_0_x(callerID, connection, userID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isLocalUserMuted_v_1_0_x(callerID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isLocalUserDeafened_v_1_0_x(callerID, deafened, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserHash_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserHash_v_1_0_x(callerID, connection, userID, hash, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getServerHash_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, const char **hash) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getServerHash_v_1_0_x(callerID, connection, hash, &promise);
+
+ return future.get();
+}
+
+
+mumble_error_t PLUGIN_CALLING_CONVENTION
+ requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserComment_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **comment) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserComment_v_1_0_x(callerID, connection, userID, comment, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID,
+ const char **description) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelDescription_v_1_0_x(callerID, connection, channelID, description, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestUserMove_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t channelID, const char *password) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestUserMove_v_1_0_x(callerID, connection, userID, channelID, password, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID,
+ bool activate) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestMicrophoneActivationOverwrite_v_1_0_x(callerID, activate, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, bool muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalMute_v_1_0_x(callerID, connection, userID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserMute_v_1_0_x(callerID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserDeaf_v_1_0_x(callerID, deafened, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ const char *comment) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestSetLocalUserComment_v_1_0_x(callerID, connection, comment, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION findUserByName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, const char *userName,
+ mumble_userid_t *userID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().findUserByName_v_1_0_x(callerID, connection, userName, userID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION findChannelByName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ const char *channelName,
+ mumble_channelid_t *channelID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().findChannelByName_v_1_0_x(callerID, connection, channelName, channelID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, bool *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_bool_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, int64_t *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_int_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, double *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_double_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key,
+ const char **outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_string_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, bool value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_bool_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, int64_t value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_int_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, double value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_double_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, const char *value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_string_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const mumble_userid_t *users, size_t userCount,
+ const uint8_t *data, size_t dataLength, const char *dataID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().sendData_v_1_0_x(callerID, connection, users, userCount, data, dataLength, dataID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION log_v_1_0_x(mumble_plugin_id_t callerID, const char *message) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().log_v_1_0_x(callerID, message, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().playSample_v_1_0_x(callerID, samplePath, &promise);
+
+ return future.get();
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////// GETTER FOR API STRUCTS /////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x() {
+ return { freeMemory_v_1_0_x,
+ getActiveServerConnection_v_1_0_x,
+ isConnectionSynchronized_v_1_0_x,
+ getLocalUserID_v_1_0_x,
+ getUserName_v_1_0_x,
+ getChannelName_v_1_0_x,
+ getAllUsers_v_1_0_x,
+ getAllChannels_v_1_0_x,
+ getChannelOfUser_v_1_0_x,
+ getUsersInChannel_v_1_0_x,
+ getLocalUserTransmissionMode_v_1_0_x,
+ isUserLocallyMuted_v_1_0_x,
+ isLocalUserMuted_v_1_0_x,
+ isLocalUserDeafened_v_1_0_x,
+ getUserHash_v_1_0_x,
+ getServerHash_v_1_0_x,
+ getUserComment_v_1_0_x,
+ getChannelDescription_v_1_0_x,
+ requestLocalUserTransmissionMode_v_1_0_x,
+ requestUserMove_v_1_0_x,
+ requestMicrophoneActivationOverwrite_v_1_0_x,
+ requestLocalMute_v_1_0_x,
+ requestLocalUserMute_v_1_0_x,
+ requestLocalUserDeaf_v_1_0_x,
+ requestSetLocalUserComment_v_1_0_x,
+ findUserByName_v_1_0_x,
+ findChannelByName_v_1_0_x,
+ getMumbleSetting_bool_v_1_0_x,
+ getMumbleSetting_int_v_1_0_x,
+ getMumbleSetting_double_v_1_0_x,
+ getMumbleSetting_string_v_1_0_x,
+ setMumbleSetting_bool_v_1_0_x,
+ setMumbleSetting_int_v_1_0_x,
+ setMumbleSetting_double_v_1_0_x,
+ setMumbleSetting_string_v_1_0_x,
+ sendData_v_1_0_x,
+ log_v_1_0_x,
+ playSample_v_1_0_x };
+}
+
+#define MAP(qtName, apiName) \
+ case Qt::Key_##qtName: \
+ return KC_##apiName
+
+mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode) {
+ switch (keyCode) {
+ MAP(Escape, ESCAPE);
+ MAP(Tab, TAB);
+ MAP(Backspace, BACKSPACE);
+ case Qt::Key_Return:
+ // Fallthrough
+ case Qt::Key_Enter:
+ return KC_ENTER;
+ MAP(Delete, DELETE);
+ MAP(Print, PRINT);
+ MAP(Home, HOME);
+ MAP(End, END);
+ MAP(Up, UP);
+ MAP(Down, DOWN);
+ MAP(Left, LEFT);
+ MAP(Right, RIGHT);
+ MAP(PageUp, PAGE_UP);
+ MAP(PageDown, PAGE_DOWN);
+ MAP(Shift, SHIFT);
+ MAP(Control, CONTROL);
+ MAP(Meta, META);
+ MAP(Alt, ALT);
+ MAP(AltGr, ALT_GR);
+ MAP(CapsLock, CAPSLOCK);
+ MAP(NumLock, NUMLOCK);
+ MAP(ScrollLock, SCROLLLOCK);
+ MAP(F1, F1);
+ MAP(F2, F2);
+ MAP(F3, F3);
+ MAP(F4, F4);
+ MAP(F5, F5);
+ MAP(F6, F6);
+ MAP(F7, F7);
+ MAP(F8, F8);
+ MAP(F9, F9);
+ MAP(F10, F10);
+ MAP(F11, F11);
+ MAP(F12, F12);
+ MAP(F13, F13);
+ MAP(F14, F14);
+ MAP(F15, F15);
+ MAP(F16, F16);
+ MAP(F17, F17);
+ MAP(F18, F18);
+ MAP(F19, F19);
+ case Qt::Key_Super_L:
+ // Fallthrough
+ case Qt::Key_Super_R:
+ return KC_SUPER;
+ MAP(Space, SPACE);
+ MAP(Exclam, EXCLAMATION_MARK);
+ MAP(QuoteDbl, DOUBLE_QUOTE);
+ MAP(NumberSign, HASHTAG);
+ MAP(Dollar, DOLLAR);
+ MAP(Percent, PERCENT);
+ MAP(Ampersand, AMPERSAND);
+ MAP(Apostrophe, SINGLE_QUOTE);
+ MAP(ParenLeft, OPEN_PARENTHESIS);
+ MAP(ParenRight, CLOSE_PARENTHESIS);
+ MAP(Asterisk, ASTERISK);
+ MAP(Plus, PLUS);
+ MAP(Comma, COMMA);
+ MAP(Minus, MINUS);
+ MAP(Period, PERIOD);
+ MAP(Slash, SLASH);
+ MAP(0, 0);
+ MAP(1, 1);
+ MAP(2, 2);
+ MAP(3, 3);
+ MAP(4, 4);
+ MAP(5, 5);
+ MAP(6, 6);
+ MAP(7, 7);
+ MAP(8, 8);
+ MAP(9, 9);
+ MAP(Colon, COLON);
+ MAP(Semicolon, SEMICOLON);
+ MAP(Less, LESS_THAN);
+ MAP(Equal, EQUALS);
+ MAP(Greater, GREATER_THAN);
+ MAP(Question, QUESTION_MARK);
+ MAP(At, AT_SYMBOL);
+ MAP(A, A);
+ MAP(B, B);
+ MAP(C, C);
+ MAP(D, D);
+ MAP(E, E);
+ MAP(F, F);
+ MAP(G, G);
+ MAP(H, H);
+ MAP(I, I);
+ MAP(J, J);
+ MAP(K, K);
+ MAP(L, L);
+ MAP(M, M);
+ MAP(N, N);
+ MAP(O, O);
+ MAP(P, P);
+ MAP(Q, Q);
+ MAP(R, R);
+ MAP(S, S);
+ MAP(T, T);
+ MAP(U, U);
+ MAP(V, V);
+ MAP(W, W);
+ MAP(X, X);
+ MAP(Y, Y);
+ MAP(Z, Z);
+ MAP(BracketLeft, OPEN_BRACKET);
+ MAP(BracketRight, CLOSE_BRACKET);
+ MAP(Backslash, BACKSLASH);
+ MAP(AsciiCircum, CIRCUMFLEX);
+ MAP(Underscore, UNDERSCORE);
+ MAP(BraceLeft, OPEN_BRACE);
+ MAP(BraceRight, CLOSE_BRACE);
+ MAP(Bar, VERTICAL_BAR);
+ MAP(AsciiTilde, TILDE);
+ MAP(degree, DEGREE_SIGN);
+ }
+
+ return KC_INVALID;
+}
+
+#undef MAP
+
+
+// Implementation of PluginData
+PluginData::PluginData() : overwriteMicrophoneActivation(false) {
+}
+
+PluginData::~PluginData() {
+}
+
+PluginData &PluginData::get() {
+ static PluginData *instance = new PluginData();
+
+ return *instance;
+}
+}; // namespace API
+
+#undef EXIT_WITH
+#undef VERIFY_PLUGIN_ID
+#undef VERIFY_CONNECTION
+#undef ENSURE_CONNECTION_SYNCHRONIZED
+#undef UNUSED
diff --git a/src/mumble/Audio.cpp b/src/mumble/Audio.cpp
index 9d383ffe7..b2d61025d 100644
--- a/src/mumble/Audio.cpp
+++ b/src/mumble/Audio.cpp
@@ -13,6 +13,7 @@
#endif
#include "Log.h"
#include "PacketDataStream.h"
+#include "PluginManager.h"
#include "Global.h"
#include <QtCore/QObject>
@@ -269,6 +270,15 @@ void Audio::stopInput() {
void Audio::start(const QString &input, const QString &output) {
startInput(input);
startOutput(output);
+
+ // Now that the audio input and output is created, we connect them to the PluginManager
+ // As these callbacks might want to change the audio before it gets further processed, all these connections have to be direct
+ QObject::connect(Global::get().ai.get(), &AudioInput::audioInputEncountered, Global::get().pluginManager,
+ &PluginManager::on_audioInput, Qt::DirectConnection);
+ QObject::connect(Global::get().ao.get(), &AudioOutput::audioSourceFetched, Global::get().pluginManager,
+ &PluginManager::on_audioSourceFetched, Qt::DirectConnection);
+ QObject::connect(Global::get().ao.get(), &AudioOutput::audioOutputAboutToPlay, Global::get().pluginManager,
+ &PluginManager::on_audioOutputAboutToPlay, Qt::DirectConnection);
}
void Audio::stop() {
diff --git a/src/mumble/AudioInput.cpp b/src/mumble/AudioInput.cpp
index d898ca0cc..cf1ca3820 100644
--- a/src/mumble/AudioInput.cpp
+++ b/src/mumble/AudioInput.cpp
@@ -11,14 +11,18 @@
# include "OpusCodec.h"
#endif
#include "MainWindow.h"
+#include "User.h"
+#include "PacketDataStream.h"
+#include "PluginManager.h"
#include "Message.h"
#include "NetworkConfig.h"
#include "PacketDataStream.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#include "User.h"
#include "Utils.h"
#include "VoiceRecorder.h"
+#include "API.h"
+
#include "Global.h"
#ifdef USE_RNNOISE
@@ -1058,7 +1062,7 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) {
iHoldFrames = 0;
}
- if (Global::get().s.atTransmit == Settings::Continuous) {
+ if (Global::get().s.atTransmit == Settings::Continuous || API::PluginData::get().overwriteMicrophoneActivation.load()) {
// Continous transmission is enabled
bIsSpeech = true;
} else if (Global::get().s.atTransmit == Settings::PushToTalk) {
@@ -1143,6 +1147,8 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) {
EncodingOutputBuffer buffer;
Q_ASSERT(buffer.size() >= static_cast< size_t >(iAudioQuality / 100 * iAudioFrames / 8));
+ emit audioInputEncountered(psSource, iFrameSize, iMicChannels, SAMPLE_RATE, bIsSpeech);
+
int len = 0;
bool encoded = true;
@@ -1274,10 +1280,13 @@ void AudioInput::flushCheck(const QByteArray &frame, bool terminator, int voiceT
}
}
- if (Global::get().s.bTransmitPosition && Global::get().p && !Global::get().bCenterPosition && Global::get().p->fetch()) {
- pds << Global::get().p->fPosition[0];
- pds << Global::get().p->fPosition[1];
- pds << Global::get().p->fPosition[2];
+ if (Global::get().s.bTransmitPosition && Global::get().pluginManager && !Global::get().bCenterPosition
+ && Global::get().pluginManager->fetchPositionalData()) {
+ Position3D currentPos = Global::get().pluginManager->getPositionalData().getPlayerPos();
+
+ pds << currentPos.x;
+ pds << currentPos.y;
+ pds << currentPos.z;
}
sendAudioFrame(data, pds);
diff --git a/src/mumble/AudioInput.h b/src/mumble/AudioInput.h
index b9dbb0c4e..0410db421 100644
--- a/src/mumble/AudioInput.h
+++ b/src/mumble/AudioInput.h
@@ -254,6 +254,14 @@ protected:
signals:
void doDeaf();
void doMute();
+ /// A signal emitted if audio input is being encountered
+ ///
+ /// @param inputPCM The encountered input PCM
+ /// @param sampleCount The amount of samples in the input
+ /// @param channelCount The amount of channels in the input
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the inpu to be speech
+ void audioInputEncountered(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech);
public:
typedef enum { ActivityStateIdle, ActivityStateReturnedFromIdle, ActivityStateActive } ActivityState;
diff --git a/src/mumble/AudioOutput.cpp b/src/mumble/AudioOutput.cpp
index 15a01600e..784a29245 100644
--- a/src/mumble/AudioOutput.cpp
+++ b/src/mumble/AudioOutput.cpp
@@ -12,7 +12,7 @@
#include "ChannelListener.h"
#include "Message.h"
#include "PacketDataStream.h"
-#include "Plugins.h"
+#include "PluginManager.h"
#include "ServerHandler.h"
#include "SpeechFlags.h"
#include "Timer.h"
@@ -395,20 +395,19 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
prioritySpeakerActive = true;
}
- if (!qlMix.isEmpty()) {
+ // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output. Otherwise we'll have to
+ // use an intermediate buffer which we will convert to an array of shorts later
+ STACKVAR(float, fOutput, iChannels * frameCount);
+ float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast<float *>(outbuff) : fOutput;
+ memset(output, 0, sizeof(float) * frameCount * iChannels);
+
+ if (! qlMix.isEmpty()) {
// There are audio sources available -> mix those sources together and feed them into the audio backend
STACKVAR(float, speaker, iChannels * 3);
STACKVAR(float, svol, iChannels);
- STACKVAR(float, fOutput, iChannels *frameCount);
-
- // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output.
- // Otherwise we'll have to use an intermediate buffer which we will convert to an array of shorts later
- float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast< float * >(outbuff) : fOutput;
bool validListener = false;
- memset(output, 0, sizeof(float) * frameCount * iChannels);
-
// Initialize recorder if recording is enabled
boost::shared_array< float > recbuff;
if (recorder) {
@@ -420,75 +419,56 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
for (unsigned int i = 0; i < iChannels; ++i)
svol[i] = mul * fSpeakerVolume[i];
- if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().p->fetch()
- && (Global::get().bPosTest || Global::get().p->fCameraPosition[0] != 0 || Global::get().p->fCameraPosition[1] != 0
- || Global::get().p->fCameraPosition[2] != 0)) {
+ if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().pluginManager->fetchPositionalData()) {
// Calculate the positional audio effects if it is enabled
- float front[3] = { Global::get().p->fCameraFront[0], Global::get().p->fCameraFront[1], Global::get().p->fCameraFront[2] };
- float top[3] = { Global::get().p->fCameraTop[0], Global::get().p->fCameraTop[1], Global::get().p->fCameraTop[2] };
-
- // Front vector is dominant; if it's zero we presume all is zero.
+ Vector3D cameraDir = Global::get().pluginManager->getPositionalData().getCameraDir();
- float flen = sqrtf(front[0] * front[0] + front[1] * front[1] + front[2] * front[2]);
+ Vector3D cameraAxis = Global::get().pluginManager->getPositionalData().getCameraAxis();
- if (flen > 0.0f) {
- front[0] *= (1.0f / flen);
- front[1] *= (1.0f / flen);
- front[2] *= (1.0f / flen);
+ // Direction vector is dominant; if it's zero we presume all is zero.
- float tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]);
+ if (!cameraDir.isZero()) {
+ cameraDir.normalize();
- if (tlen > 0.0f) {
- top[0] *= (1.0f / tlen);
- top[1] *= (1.0f / tlen);
- top[2] *= (1.0f / tlen);
+ if (!cameraAxis.isZero()) {
+ cameraAxis.normalize();
} else {
- top[0] = 0.0f;
- top[1] = 1.0f;
- top[2] = 0.0f;
+ cameraAxis = { 0.0f, 1.0f, 0.0f };
}
- const float dotproduct = front[0] * top[0] + front[1] * top[1] + front[2] * top[2];
+ const float dotproduct = cameraDir.dotProduct(cameraAxis);
const float error = std::abs(dotproduct);
if (error > 0.5f) {
// Not perpendicular by a large margin. Assume Y up and rotate 90 degrees.
float azimuth = 0.0f;
- if ((front[0] != 0.0f) || (front[2] != 0.0f))
- azimuth = atan2f(front[2], front[0]);
- float inclination = acosf(front[1]) - static_cast< float >(M_PI) / 2.0f;
+ if (cameraDir.x != 0.0f || cameraDir.z != 0.0f) {
+ azimuth = atan2f(cameraDir.z, cameraDir.x);
+ }
+
+ float inclination = acosf(cameraDir.y) - static_cast< float >(M_PI) / 2.0f;
- top[0] = sinf(inclination) * cosf(azimuth);
- top[1] = cosf(inclination);
- top[2] = sinf(inclination) * sinf(azimuth);
+ cameraAxis.x = sinf(inclination) * cosf(azimuth);
+ cameraAxis.y = cosf(inclination);
+ cameraAxis.z = sinf(inclination) * sinf(azimuth);
} else if (error > 0.01f) {
// Not perpendicular by a small margin. Find the nearest perpendicular vector.
+ cameraAxis = cameraAxis - cameraDir * dotproduct;
- top[0] -= front[0] * dotproduct;
- top[1] -= front[1] * dotproduct;
- top[2] -= front[2] * dotproduct;
-
- // normalize top again
- tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]);
- // tlen is guaranteed to be non-zero, otherwise error would have been larger than 0.5
- top[0] *= (1.0f / tlen);
- top[1] *= (1.0f / tlen);
- top[2] *= (1.0f / tlen);
+ // normalize axis again (the orthogonalized vector us guaranteed to be non-zero
+ // as the error (dotproduct) was only 0.5 (and not 1 in which case above operation
+ // would create the zero-vector).
+ cameraAxis.normalize();
}
} else {
- front[0] = 0.0f;
- front[1] = 0.0f;
- front[2] = 1.0f;
+ cameraDir = { 0.0f, 0.0f, 1.0f };
- top[0] = 0.0f;
- top[1] = 1.0f;
- top[2] = 0.0f;
+ cameraAxis = { 0.0f, 1.0f, 0.0f };
}
// Calculate right vector as front X top
- float right[3] = { top[1] * front[2] - top[2] * front[1], top[2] * front[0] - top[0] * front[2],
- top[0] * front[1] - top[1] * front[0] };
+ Vector3D right = cameraAxis.crossProduct(cameraDir);
/*
qWarning("Front: %f %f %f", front[0], front[1], front[2]);
@@ -497,26 +477,27 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
*/
// Rotate speakers to match orientation
for (unsigned int i = 0; i < iChannels; ++i) {
- speaker[3 * i + 0] =
- fSpeakers[3 * i + 0] * right[0] + fSpeakers[3 * i + 1] * top[0] + fSpeakers[3 * i + 2] * front[0];
- speaker[3 * i + 1] =
- fSpeakers[3 * i + 0] * right[1] + fSpeakers[3 * i + 1] * top[1] + fSpeakers[3 * i + 2] * front[1];
- speaker[3 * i + 2] =
- fSpeakers[3 * i + 0] * right[2] + fSpeakers[3 * i + 1] * top[2] + fSpeakers[3 * i + 2] * front[2];
+ speaker[3 * i + 0] = fSpeakers[3 * i + 0] * right.x + fSpeakers[3 * i + 1] * cameraAxis.x
+ + fSpeakers[3 * i + 2] * cameraDir.x;
+ speaker[3 * i + 1] = fSpeakers[3 * i + 0] * right.y + fSpeakers[3 * i + 1] * cameraAxis.y
+ + fSpeakers[3 * i + 2] * cameraDir.y;
+ speaker[3 * i + 2] = fSpeakers[3 * i + 0] * right.z + fSpeakers[3 * i + 1] * cameraAxis.z
+ + fSpeakers[3 * i + 2] * cameraDir.z;
}
validListener = true;
}
foreach (AudioOutputUser *aop, qlMix) {
// Iterate through all audio sources and mix them together into the output (or the intermediate array)
- const float *RESTRICT pfBuffer = aop->pfBuffer;
- float volumeAdjustment = 1;
+ float *RESTRICT pfBuffer = aop->pfBuffer;
+ float volumeAdjustment = 1;
// Check if the audio source is a user speaking (instead of a sample playback) and apply potential volume
// adjustments
AudioOutputSpeech *speech = qobject_cast< AudioOutputSpeech * >(aop);
+ const ClientUser *user = nullptr;
if (speech) {
- const ClientUser *user = speech->p;
+ user = speech->p;
volumeAdjustment *= user->getLocalVolumeAdjustments();
if (user->cChannel && ChannelListener::isListening(Global::get().uiSession, user->cChannel->iId)
@@ -534,6 +515,11 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
}
}
+ // As the events may cause the output PCM to change, the connection has to be direct in any case
+ const int channels = (speech && speech->bStereo) ? 2 : 1;
+ // If user != nullptr, then the current audio is considered speech
+ emit audioSourceFetched(pfBuffer, frameCount, channels, SAMPLE_RATE, static_cast< bool >(user), user);
+
// If recording is enabled add the current audio source to the recording buffer
if (recorder) {
if (speech) {
@@ -574,27 +560,33 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
#endif
// If positional audio is enabled, calculate the respective audio effect here
- float dir[3] = { aop->fPos[0] - Global::get().p->fCameraPosition[0], aop->fPos[1] - Global::get().p->fCameraPosition[1],
- aop->fPos[2] - Global::get().p->fCameraPosition[2] };
- float len = sqrtf(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
+ Position3D outputPos = { aop->fPos[0], aop->fPos[1], aop->fPos[2] };
+ Position3D ownPos = Global::get().pluginManager->getPositionalData().getCameraPos();
+
+ Vector3D connectionVec = outputPos - ownPos;
+ float len = connectionVec.norm();
+
if (len > 0.0f) {
- dir[0] /= len;
- dir[1] /= len;
- dir[2] /= len;
+ // Don't use normalize-func in order to save the re-computation of the vector's length
+ connectionVec.x /= len;
+ connectionVec.y /= len;
+ connectionVec.z /= len;
}
/*
qWarning("Voice pos: %f %f %f", aop->fPos[0], aop->fPos[1], aop->fPos[2]);
- qWarning("Voice dir: %f %f %f", dir[0], dir[1], dir[2]);
+ qWarning("Voice dir: %f %f %f", connectionVec.x, connectionVec.y, connectionVec.z);
*/
if (!aop->pfVolume) {
aop->pfVolume = new float[nchan];
for (unsigned int s = 0; s < nchan; ++s)
aop->pfVolume[s] = -1.0;
}
+
for (unsigned int s = 0; s < nchan; ++s) {
- const float dot = bSpeakerPositional[s] ? dir[0] * speaker[s * 3 + 0] + dir[1] * speaker[s * 3 + 1]
- + dir[2] * speaker[s * 3 + 2]
- : 1.0f;
+ const float dot = bSpeakerPositional[s]
+ ? connectionVec.x * speaker[s * 3 + 0] + connectionVec.y * speaker[s * 3 + 1]
+ + connectionVec.z * speaker[s * 3 + 2]
+ : 1.0f;
const float str = svol[s] * calcGain(dot, len) * volumeAdjustment;
float *RESTRICT o = output + s;
const float old = (aop->pfVolume[s] >= 0.0f) ? aop->pfVolume[s] : str;
@@ -642,7 +634,12 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
if (recorder && recorder->isInMixDownMode()) {
recorder->addBuffer(nullptr, recbuff, frameCount);
}
+ }
+
+ bool pluginModifiedAudio = false;
+ emit audioOutputAboutToPlay(output, frameCount, nchan, SAMPLE_RATE, &pluginModifiedAudio);
+ if (pluginModifiedAudio || (! qlMix.isEmpty())) {
// Clip the output audio
if (eSampleFormat == SampleFloat)
for (unsigned int i = 0; i < frameCount * iChannels; i++)
@@ -665,7 +662,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
#endif
// Return whether data has been written to the outbuff
- return (!qlMix.isEmpty());
+ return (pluginModifiedAudio || (! qlMix.isEmpty()));
}
bool AudioOutput::isAlive() const {
diff --git a/src/mumble/AudioOutput.h b/src/mumble/AudioOutput.h
index 299736b9d..3d5c5b6da 100644
--- a/src/mumble/AudioOutput.h
+++ b/src/mumble/AudioOutput.h
@@ -127,6 +127,25 @@ public:
static float calcGain(float dotproduct, float distance);
unsigned int getMixerFreq() const;
void setBufferSize(unsigned int bufferSize);
+
+signals:
+ /// Signal emitted whenever an audio source has been fetched
+ ///
+ /// @param outputPCM The fetched output PCM
+ /// @param sampleCount The amount of samples in the output
+ /// @param channelCount The amount of channels in the output
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether the fetched output is considered to be speech
+ /// @param A pointer to the user that this speech belongs to or nullptr if this isn't speech
+ void audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser *user);
+ /// Signal emitted whenever an audio is about to be played to the user
+ ///
+ /// @param outputPCM The output PCM that is to be played
+ /// @param sampleCount The amount of samples in the output
+ /// @param channelCount The amount of channels in the output
+ /// @param sampleRate The used sample rate in Hz
+ /// @param modifiedAudio Pointer to bool if audio has been modified or not and should be played
+ void audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio);
};
#endif
diff --git a/src/mumble/CMakeLists.txt b/src/mumble/CMakeLists.txt
index d904be2b1..cf28c1dd5 100644
--- a/src/mumble/CMakeLists.txt
+++ b/src/mumble/CMakeLists.txt
@@ -35,6 +35,9 @@ option(qtspeech "Use Qt's text-to-speech system (part of the Qt Speech module) i
option(jackaudio "Build support for JackAudio." ON)
option(portaudio "Build support for PortAudio" ON)
+option(plugin-debug "Build Mumble with debug output for plugin developers." OFF)
+option(plugin-callback-debug "Build Mumble with debug output for plugin callbacks inside of Mumble." OFF)
+
if(WIN32)
option(asio "Build support for ASIO audio input." OFF)
option(wasapi "Build support for WASAPI." ON)
@@ -81,6 +84,8 @@ set(MUMBLE_SOURCES
"ACLEditor.cpp"
"ACLEditor.h"
"ACLEditor.ui"
+ "API_v_1_0_x.cpp"
+ "API.h"
"ApplicationPalette.h"
"AudioConfigDialog.cpp"
"AudioConfigDialog.h"
@@ -142,6 +147,8 @@ set(MUMBLE_SOURCES
"LCD.cpp"
"LCD.h"
"LCD.ui"
+ "LegacyPlugin.cpp"
+ "LegacyPlugin.h"
"ListenerLocalVolumeDialog.cpp"
"Log.cpp"
"Log.h"
@@ -162,9 +169,21 @@ set(MUMBLE_SOURCES
"NetworkConfig.ui"
"OpusCodec.cpp"
"OpusCodec.h"
- "Plugins.cpp"
- "Plugins.h"
- "Plugins.ui"
+ "PluginConfig.cpp"
+ "PluginConfig.h"
+ "PluginConfig.ui"
+ "Plugin.cpp"
+ "Plugin.h"
+ "PluginInstaller.cpp"
+ "PluginInstaller.h"
+ "PluginInstaller.ui"
+ "PluginManager.cpp"
+ "PluginManager.h"
+ "PluginUpdater.cpp"
+ "PluginUpdater.h"
+ "PluginUpdater.ui"
+ "PositionalData.cpp"
+ "PositionalData.h"
"PTTButtonWidget.cpp"
"PTTButtonWidget.h"
"PTTButtonWidget.ui"
@@ -347,8 +366,72 @@ target_include_directories(mumble
"widgets"
${SHARED_SOURCE_DIR}
"${3RDPARTY_DIR}/smallft"
+ "${PLUGINS_DIR}"
)
+find_pkg(Poco COMPONENTS Zip)
+
+if(TARGET Poco::Zip)
+ target_link_libraries(mumble
+ PRIVATE
+ Poco::Zip
+ )
+else()
+ message(STATUS "Regular Poco search failed - looking for Poco include dir manually...")
+
+ if(MINGW)
+ # These are the paths for our MXE environment
+ if(32_BIT)
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/i686-w64-mingw32.static/include/")
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/x86_64-w64-mingw32.static/include/")
+ endif()
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/include")
+ endif()
+
+ find_path(POCO_INCLUDE_DIR "Poco/Poco.h" HINTS ${POCO_INCLUDE_DIR_HINT})
+
+ if(POCO_INCLUDE_DIR)
+ message(STATUS "Found Poco include dir at \"${POCO_INCLUDE_DIR}\"")
+ else()
+ message(FATAL_ERROR "Unable to locate Poco include directory")
+ endif()
+
+ find_library(POCO_LIB_FOUNDATION NAMES PocoFoundation PocoFoundationmd REQUIRED)
+ find_library(POCO_LIB_UTIL NAMES PocoUtil PocoUtilmd REQUIRED)
+ find_library(POCO_LIB_XML NAMES PocoXML PocoXMLmd REQUIRED)
+ find_library(POCO_LIB_ZIP NAMES PocoZip PocoZipmd REQUIRED)
+
+ if(POCO_LIB_ZIP)
+ message(STATUS "Found Poco Zip library at \"${POCO_LIB_ZIP}\"")
+ else()
+ message(FATAL_ERROR "Unable to find Poco Zip library")
+ endif()
+
+
+ # Now use the found include dir and libraries by linking it to the target
+ target_include_directories(mumble
+ PRIVATE
+ ${POCO_INCLUDE_DIR}
+ )
+
+ target_link_libraries(mumble
+ PRIVATE
+ ${POCO_LIB_ZIP}
+ ${POCO_LIB_XML}
+ ${POCO_LIB_UTIL}
+ ${POCO_LIB_FOUNDATION}
+ )
+
+ if(static)
+ target_compile_definitions(mumble
+ PUBLIC
+ POCO_STATIC
+ )
+ endif()
+endif()
+
find_pkg("SndFile;LibSndFile;sndfile" REQUIRED)
# Look for various targets as they are named differently on different platforms
@@ -1020,3 +1103,21 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
)
endif()
endif()
+
+if(plugin-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_DEBUG")
+endif()
+
+if(plugin-callback-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_CALLBACK_DEBUG")
+endif()
+
+if(UNIX)
+ if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+ # On FreeBSD we need the util library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE util)
+ elseif(${CMAKE_SYSTEM_NAME} MATCHES ".*BSD")
+ # On any other BSD we need the kvm library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE kvm)
+ endif()
+endif()
diff --git a/src/mumble/ClientUser.cpp b/src/mumble/ClientUser.cpp
index 3e7c7152c..8a66cf496 100644
--- a/src/mumble/ClientUser.cpp
+++ b/src/mumble/ClientUser.cpp
@@ -7,6 +7,7 @@
#include "AudioOutput.h"
#include "Channel.h"
+#include "PluginManager.h"
#include "Global.h"
QHash< unsigned int, ClientUser * > ClientUser::c_qmUsers;
@@ -61,6 +62,9 @@ ClientUser *ClientUser::add(unsigned int uiSession, QObject *po) {
ClientUser *p = new ClientUser(po);
p->uiSession = uiSession;
c_qmUsers[uiSession] = p;
+
+ QObject::connect(p, &ClientUser::talkingStateChanged, Global::get().pluginManager, &PluginManager::on_userTalkingStateChanged);
+
return p;
}
diff --git a/src/mumble/Global.cpp b/src/mumble/Global.cpp
index acbbec7e1..b8c001ad1 100644
--- a/src/mumble/Global.cpp
+++ b/src/mumble/Global.cpp
@@ -73,7 +73,7 @@ static void migrateDataDir() {
Global::Global(const QString &qsConfigPath) {
mw = 0;
db = 0;
- p = 0;
+ pluginManager = 0;
nam = 0;
c = 0;
talkingUI = 0;
diff --git a/src/mumble/Global.h b/src/mumble/Global.h
index 5aee1abf5..d8748bb51 100644
--- a/src/mumble/Global.h
+++ b/src/mumble/Global.h
@@ -22,7 +22,7 @@ class AudioInput;
class AudioOutput;
class Database;
class Log;
-class Plugins;
+class PluginManager;
class QSettings;
class Overlay;
class LCD;
@@ -53,7 +53,8 @@ public:
*/
Database *db;
Log *l;
- Plugins *p;
+ /// A pointer to the PluginManager that is used in this session
+ PluginManager *pluginManager;
QSettings *qs;
#ifdef USE_OVERLAY
Overlay *o;
diff --git a/src/mumble/LegacyPlugin.cpp b/src/mumble/LegacyPlugin.cpp
new file mode 100644
index 000000000..420f3cf73
--- /dev/null
+++ b/src/mumble/LegacyPlugin.cpp
@@ -0,0 +1,267 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "LegacyPlugin.h"
+#include "MumblePlugin_v_1_0_x.h"
+
+#include <cstdlib>
+#include <wchar.h>
+#include <map>
+#include <string.h>
+#include <codecvt>
+#include <locale>
+
+#include <QRegularExpression>
+
+
+/// A regular expression used to extract the version from the legacy plugin's description
+static const QRegularExpression versionRegEx(QString::fromLatin1("(?:v)?(?:ersion)?[ \\t]*(\\d+)\\.(\\d+)(?:\\.(\\d+))?"), QRegularExpression::CaseInsensitiveOption);
+
+
+LegacyPlugin::LegacyPlugin(QString path, bool isBuiltIn, QObject *p)
+ : Plugin(path, isBuiltIn, p),
+ m_name(),
+ m_description(),
+ m_version(VERSION_UNKNOWN),
+ m_mumPlug(0),
+ m_mumPlug2(0),
+ m_mumPlugQt(0) {
+}
+
+LegacyPlugin::~LegacyPlugin() {
+}
+
+bool LegacyPlugin::doInitialize() {
+ if (Plugin::doInitialize()) {
+ // initialization seems to have succeeded so far
+ // This means that mumPlug is initialized
+
+ m_name = QString::fromStdWString(m_mumPlug->shortname);
+ // Although the MumblePlugin struct has a member called "description", the actual description seems to
+ // always only be returned by the longdesc function (The description member is actually just the name with some version
+ // info)
+ m_description = QString::fromStdWString(m_mumPlug->longdesc());
+ // The version field in the MumblePlugin2 struct is the positional-audio-plugin-API version and not the version
+ // of the plugin itself. This information is not provided for legacy plugins.
+ // Most of them however provide information about the version of the game they support. Thus we will try to parse the
+ // description and extract this version using it for the plugin's version as well.
+ // Some plugins have the version in the actual description field of the old API (see above comment why these aren't the same)
+ // so we will use a combination of both to search for the version. If multiple version(-like) strings are found, the last one
+ // will be used.
+ QString matchContent = m_description + QChar::Null + QString::fromStdWString(m_mumPlug->description);
+ QRegularExpressionMatchIterator matchIt = versionRegEx.globalMatch(matchContent);
+
+ // Only consider the last match
+ QRegularExpressionMatch match;
+ while (matchIt.hasNext()) {
+ match = matchIt.next();
+ }
+
+ if (match.hasMatch()) {
+ // Store version
+ m_version = { match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt() };
+ }
+
+ return true;
+ } else {
+ // initialization has failed
+ // pass on info about failed init
+ return false;
+ }
+}
+
+void LegacyPlugin::resolveFunctionPointers() {
+ // We don't set any functions inside the apiFnc struct variable in order for the default
+ // implementations in the Plugin class to mimic empty default implementations for all functions
+ // not explicitly overwritten by this class
+
+ if (isValid()) {
+ // The corresponding library was loaded -> try to locate all API functions of the legacy plugin's spec
+ // (for positional audio) and set defaults for the other ones in order to maintain compatibility with
+ // the new plugin system
+
+ QWriteLocker lock(&m_pluginLock);
+
+ mumblePluginFunc pluginFunc = reinterpret_cast<mumblePluginFunc>(m_lib.resolve("getMumblePlugin"));
+ mumblePlugin2Func plugin2Func = reinterpret_cast<mumblePlugin2Func>(m_lib.resolve("getMumblePlugin2"));
+ mumblePluginQtFunc pluginQtFunc = reinterpret_cast<mumblePluginQtFunc>(m_lib.resolve("getMumblePluginQt"));
+
+ if (pluginFunc) {
+ m_mumPlug = pluginFunc();
+ }
+ if (plugin2Func) {
+ m_mumPlug2 = plugin2Func();
+ }
+ if (pluginQtFunc) {
+ m_mumPlugQt = pluginQtFunc();
+ }
+
+ // A legacy plugin is valid as long as there is a function to get the MumblePlugin struct from it
+ // and the plugin has been compiled by the same compiler as this client (determined by the plugin's
+ // "magic") and it isn't retracted
+ bool suitableMagic = m_mumPlug && m_mumPlug->magic == MUMBLE_PLUGIN_MAGIC;
+ bool retracted = m_mumPlug && m_mumPlug->shortname == L"Retracted";
+ m_pluginIsValid = pluginFunc && suitableMagic && !retracted;
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ if (!m_pluginIsValid) {
+ if (!pluginFunc) {
+ qDebug("Plugin \"%s\" is missing the getMumblePlugin() function", qPrintable(m_pluginPath));
+ } else if (!suitableMagic) {
+ qDebug("Plugin \"%s\" was compiled with a different compiler (magic differs)", qPrintable(m_pluginPath));
+ } else {
+ qDebug("Plugin \"%s\" is retracted", qPrintable(m_pluginPath));
+ }
+ }
+#endif
+ }
+}
+
+mumble_error_t LegacyPlugin::init() {
+ {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_pluginIsLoaded = true;
+ }
+
+ // No-op as legacy plugins never have anything to initialize
+ // The only init function they care about is the one that inits positional audio
+ return STATUS_OK;
+}
+
+QString LegacyPlugin::getName() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_name.isEmpty()) {
+ return m_name;
+ } else {
+ return QString::fromLatin1("<Unknown Legacy Plugin>");
+ }
+}
+
+QString LegacyPlugin::getDescription() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_description.isEmpty()) {
+ return m_description;
+ } else {
+ return QString::fromLatin1("<No description provided by the legacy plugin>");
+ }
+}
+
+bool LegacyPlugin::showAboutDialog(QWidget *parent) const {
+ if (m_mumPlugQt && m_mumPlugQt->about) {
+ m_mumPlugQt->about(parent);
+
+ return true;
+ }
+ if (m_mumPlug->about) {
+ // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct
+ // so we'll mimic that behaviour for compatibility
+ m_mumPlug->about(nullptr);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool LegacyPlugin::showConfigDialog(QWidget *parent) const {
+ if (m_mumPlugQt && m_mumPlugQt->config) {
+ m_mumPlugQt->config(parent);
+
+ return true;
+ }
+ if (m_mumPlug->config) {
+ // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct
+ // so we'll mimic that behaviour for compatibility
+ m_mumPlug->config(nullptr);
+
+ return true;
+ }
+
+ return false;
+}
+
+uint8_t LegacyPlugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) {
+ int retCode;
+
+ if (m_mumPlug2) {
+ // Create and populate a multimap holding the names and PIDs to pass to the tryLock-function
+ std::multimap<std::wstring, unsigned long long int> pidMap;
+
+ for (size_t i=0; i<programCount; i++) {
+ std::string currentName = programNames[i];
+ std::wstring currentNameWstr = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(currentName);
+
+ pidMap.insert(std::pair<std::wstring, unsigned long long int>(currentNameWstr, programPIDs[i]));
+ }
+
+ retCode = m_mumPlug2->trylock(pidMap);
+ } else {
+ // The default MumblePlugin doesn't take the name and PID arguments
+ retCode = m_mumPlug->trylock();
+ }
+
+ // ensure that only expected return codes are being returned from this function
+ // the legacy plugins return 1 on successfull locking and 0 on failure
+ if (retCode) {
+ QWriteLocker wLock(&m_pluginLock);
+
+ m_positionalDataIsActive = true;
+
+ return PDEC_OK;
+ } else {
+ // legacy plugins don't have the concept of indicating a permanent error
+ // so we'll return a temporary error for them
+ return PDEC_ERROR_TEMP;
+ }
+}
+
+bool LegacyPlugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const {
+ std::wstring identityWstr;
+ std::string contextStr;
+
+ int retCode = m_mumPlug->fetch(static_cast<float*>(avatarPos), static_cast<float*>(avatarDir), static_cast<float*>(avatarAxis),
+ static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(cameraAxis), contextStr, identityWstr);
+
+ context = QString::fromStdString(contextStr);
+ identity = QString::fromStdWString(identityWstr);
+
+ // The fetch-function should return if it is "still locked on" meaning that it can continue providing
+ // positional audio
+ return retCode == 1;
+}
+
+void LegacyPlugin::shutdownPositionalData() {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_positionalDataIsActive = false;
+
+ m_mumPlug->unlock();
+}
+
+uint32_t LegacyPlugin::getFeatures() const {
+ return FEATURE_POSITIONAL;
+}
+
+mumble_version_t LegacyPlugin::getVersion() const {
+ return m_version;
+}
+
+bool LegacyPlugin::providesAboutDialog() const {
+ return m_mumPlug->about || (m_mumPlugQt && m_mumPlugQt->about);
+}
+
+bool LegacyPlugin::providesConfigDialog() const {
+ return m_mumPlug->config || (m_mumPlugQt && m_mumPlugQt->config);
+}
+
+mumble_version_t LegacyPlugin::getAPIVersion() const {
+ // Legacy plugins are always on most recent API as they don't use it in any case -> no need to perform
+ // backwards compatibility stuff
+ return MUMBLE_PLUGIN_API_VERSION;
+}
diff --git a/src/mumble/LegacyPlugin.h b/src/mumble/LegacyPlugin.h
new file mode 100644
index 000000000..250b624a0
--- /dev/null
+++ b/src/mumble/LegacyPlugin.h
@@ -0,0 +1,82 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+#define MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+
+#include "Plugin.h"
+
+#include <QtCore/QString>
+
+#include <string>
+#include <memory>
+
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "mumble_legacy_plugin.h"
+
+class LegacyPlugin;
+
+/// Typedef for a LegacyPlugin pointer
+typedef std::shared_ptr<LegacyPlugin> legacy_plugin_ptr_t;
+/// Typedef for a const LegacyPlugin pointer
+typedef std::shared_ptr<const LegacyPlugin> const_legacy_plugin_ptr_t;
+
+
+/// This class is meant for compatibility for old Mumble "plugins" that stem from before the plugin framework has been
+/// introduced. Thus the "plugins" represented by this class are for positional data gathering only.
+class LegacyPlugin : public Plugin {
+ friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize()
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(LegacyPlugin)
+
+ protected:
+ /// The name of the "plugin"
+ QString m_name;
+ /// The description of the "plugin"
+ QString m_description;
+ /// The Version of the "plugin"
+ mumble_version_t m_version;
+ /// A pointer to the PluginStruct in its initial version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin *m_mumPlug;
+ /// A pointer to the PluginStruct in its second, enhanced version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin2 *m_mumPlug2;
+ /// A pointer to the PluginStruct that encorporates Qt functionality. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePluginQt *m_mumPlugQt;
+
+ virtual void resolveFunctionPointers() override;
+ virtual bool doInitialize() override;
+
+ LegacyPlugin(QString path, bool isBuiltIn = false, QObject *p = 0);
+
+ virtual bool showAboutDialog(QWidget *parent) const override;
+ virtual bool showConfigDialog(QWidget *parent) const override;
+ virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) override;
+ virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const override;
+ virtual void shutdownPositionalData() override;
+ public:
+ virtual ~LegacyPlugin() override;
+
+ virtual mumble_error_t init() override;
+
+ // functions for direct plugin-interaction
+ virtual QString getName() const override;
+
+ virtual QString getDescription() const override;
+ virtual uint32_t getFeatures() const override;
+ virtual mumble_version_t getAPIVersion() const override;
+
+ virtual mumble_version_t getVersion() const override;
+
+ // functions for checking which underlying plugin functions are implemented
+ virtual bool providesAboutDialog() const override;
+ virtual bool providesConfigDialog() const override;
+};
+
+#endif
diff --git a/src/mumble/Log.cpp b/src/mumble/Log.cpp
index d4c011197..5a2b14edc 100644
--- a/src/mumble/Log.cpp
+++ b/src/mumble/Log.cpp
@@ -334,6 +334,8 @@ QVector< LogMessage > Log::qvDeferredLogs;
Log::Log(QObject *p) : QObject(p) {
+ qRegisterMetaType<Log::MsgType>();
+
#ifndef USE_NO_TTS
tts = new TextToSpeech(this);
tts->setVolume(Global::get().s.iTTSVolume);
@@ -374,7 +376,8 @@ const Log::MsgType Log::msgOrder[] = { DebugInfo,
ChannelLeaveDisconnect,
PermissionDenied,
TextMessage,
- PrivateTextMessage };
+ PrivateTextMessage,
+ PluginMessage };
const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"),
QT_TRANSLATE_NOOP("Log", "Critical"),
@@ -406,7 +409,8 @@ const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"),
QT_TRANSLATE_NOOP("Log", "User left channel and disconnected"),
QT_TRANSLATE_NOOP("Log", "Private text message"),
QT_TRANSLATE_NOOP("Log", "User started listening to channel"),
- QT_TRANSLATE_NOOP("Log", "User stopped listening to channel") };
+ QT_TRANSLATE_NOOP("Log", "User stopped listening to channel"),
+ QT_TRANSLATE_NOOP("Log", "Plugin message") };
QString Log::msgName(MsgType t) const {
return tr(msgNames[t]);
diff --git a/src/mumble/Log.h b/src/mumble/Log.h
index 6958f2be9..d584f2a49 100644
--- a/src/mumble/Log.h
+++ b/src/mumble/Log.h
@@ -90,8 +90,10 @@ public:
ChannelLeaveDisconnect,
PrivateTextMessage,
ChannelListeningAdd,
- ChannelListeningRemove
+ ChannelListeningRemove,
+ PluginMessage
};
+
enum LogColorType { Time, Server, Privilege, Source, Target };
static const MsgType firstMsgType = DebugInfo;
static const MsgType lastMsgType = ChannelListeningRemove;
@@ -134,7 +136,9 @@ public:
static void logOrDefer(Log::MsgType mt, const QString &console, const QString &terse = QString(),
bool ownMessage = false, const QString &overrideTTS = QString(), bool ignoreTTS = false);
public slots:
- void log(MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false,
+ // We have to explicitly use Log::MsgType and not only MsgType in order to be able to use QMetaObject::invokeMethod
+ // with this function.
+ void log(Log::MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false,
const QString &overrideTTS = QString(), bool ignoreTTS = false);
/// Logs LogMessages that have been deferred so far
void processDeferredLogs();
@@ -172,4 +176,6 @@ public:
LogDocumentResourceAddedEvent();
};
+Q_DECLARE_METATYPE(Log::MsgType);
+
#endif
diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp
index e7ae820d1..f2718f0a3 100644
--- a/src/mumble/MainWindow.cpp
+++ b/src/mumble/MainWindow.cpp
@@ -33,8 +33,8 @@
#include "ChannelListener.h"
#include "ListenerLocalVolumeDialog.h"
#include "Markdown.h"
+#include "PluginManager.h"
#include "PTTButtonWidget.h"
-#include "Plugins.h"
#include "RichTextEditor.h"
#include "SSLCipherInfo.h"
#include "Screen.h"
@@ -185,6 +185,8 @@ MainWindow::MainWindow(QWidget *p) : QMainWindow(p) {
setOnTop(Global::get().s.aotbAlwaysOnTop == Settings::OnTopAlways
|| (Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInMinimal)
|| (!Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInNormal));
+
+ QObject::connect(this, &MainWindow::serverSynchronized, Global::get().pluginManager, &PluginManager::on_serverSynchronized);
}
void MainWindow::createActions() {
@@ -318,6 +320,13 @@ void MainWindow::setupGui() {
QObject::connect(&ChannelListener::get(), &ChannelListener::localVolumeAdjustmentsChanged, pmModel,
&UserModel::on_channelListenerLocalVolumeAdjustmentChanged);
+ // connect slots to PluginManager
+ QObject::connect(pmModel, &UserModel::userAdded, Global::get().pluginManager, &PluginManager::on_userAdded);
+ QObject::connect(pmModel, &UserModel::userRemoved, Global::get().pluginManager, &PluginManager::on_userRemoved);
+ QObject::connect(pmModel, &UserModel::channelAdded, Global::get().pluginManager, &PluginManager::on_channelAdded);
+ QObject::connect(pmModel, &UserModel::channelRemoved, Global::get().pluginManager, &PluginManager::on_channelRemoved);
+ QObject::connect(pmModel, &UserModel::channelRenamed, Global::get().pluginManager, &PluginManager::on_channelRenamed);
+
qaAudioMute->setChecked(Global::get().s.bMute);
qaAudioDeaf->setChecked(Global::get().s.bDeaf);
#ifdef USE_NO_TTS
@@ -902,6 +911,16 @@ static void recreateServerHandler() {
SLOT(resolverError(QAbstractSocket::SocketError, QString)));
QObject::connect(sh.get(), &ServerHandler::disconnected, Global::get().talkingUI, &TalkingUI::on_serverDisconnected);
+
+ // We have to use direct connections for these here as the PluginManager must be able to access the connection's ID
+ // and in order for that to be possible the (dis)connection process must not proceed in the background.
+ Global::get().pluginManager->connect(sh.get(), &ServerHandler::connected, Global::get().pluginManager,
+ &PluginManager::on_serverConnected, Qt::DirectConnection);
+ // We connect the plugin manager to "aboutToDisconnect" instead of "disconnect" in order for the slot to be
+ // guaranteed to be completed *before* the acutal disconnect logic (e.g. MainWindow::serverDisconnected) kicks in.
+ // In order for that to work it is ESSENTIAL to use a DIRECT CONNECTION!
+ Global::get().pluginManager->connect(sh.get(), &ServerHandler::aboutToDisconnect, Global::get().pluginManager,
+ &PluginManager::on_serverDisconnected, Qt::DirectConnection);
}
void MainWindow::openUrl(const QUrl &url) {
@@ -2491,6 +2510,12 @@ void MainWindow::on_qaAudioMute_triggered() {
updateTrayIcon();
}
+void MainWindow::setAudioMute(bool mute) {
+ // Pretend the user pushed the button manually
+ qaAudioMute->setChecked(mute);
+ qaAudioMute->triggered(mute);
+}
+
void MainWindow::on_qaAudioDeaf_triggered() {
if (Global::get().bInAudioWizard) {
qaAudioDeaf->setChecked(!qaAudioDeaf->isChecked());
@@ -2503,11 +2528,13 @@ void MainWindow::on_qaAudioDeaf_triggered() {
on_qaAudioMute_triggered();
return;
}
+
AudioInputPtr ai = Global::get().ai;
if (ai)
ai->tIdle.restart();
Global::get().s.bDeaf = qaAudioDeaf->isChecked();
+
if (Global::get().s.bDeaf && !Global::get().s.bMute) {
bAutoUnmute = true;
Global::get().s.bMute = true;
@@ -2527,6 +2554,12 @@ void MainWindow::on_qaAudioDeaf_triggered() {
updateTrayIcon();
}
+void MainWindow::setAudioDeaf(bool deaf) {
+ // Pretend the user pushed the button manually
+ qaAudioDeaf->setChecked(deaf);
+ qaAudioDeaf->triggered(deaf);
+}
+
void MainWindow::on_qaRecording_triggered() {
if (voiceRecorderDialog) {
voiceRecorderDialog->show();
@@ -2549,7 +2582,7 @@ void MainWindow::on_qaAudioStats_triggered() {
}
void MainWindow::on_qaAudioUnlink_triggered() {
- Global::get().p->bUnlink = true;
+ Global::get().pluginManager->unlinkPositionalData();
}
void MainWindow::on_qaConfigDialog_triggered() {
diff --git a/src/mumble/MainWindow.h b/src/mumble/MainWindow.h
index f600ede14..20c8a3000 100644
--- a/src/mumble/MainWindow.h
+++ b/src/mumble/MainWindow.h
@@ -311,6 +311,14 @@ public slots:
/// Updates the user's image directory to the given path (any included
/// filename is discarded).
void updateImagePath(QString filepath) const;
+ /// Sets the local user's mute state
+ ///
+ /// @param mute Whether to mute the user
+ void setAudioMute(bool mute);
+ /// Sets the local user's deaf state
+ ///
+ /// @param deaf Whether to deafen the user
+ void setAudioDeaf(bool deaf);
signals:
/// Signal emitted when the server and the client have finished
/// synchronizing (after a new connection).
diff --git a/src/mumble/ManualPlugin.cpp b/src/mumble/ManualPlugin.cpp
index 981273c5f..e4ea84336 100644
--- a/src/mumble/ManualPlugin.cpp
+++ b/src/mumble/ManualPlugin.cpp
@@ -9,13 +9,15 @@
#include "ManualPlugin.h"
#include "ui_ManualPlugin.h"
+#include "Global.h"
+
#include <QPointer>
#include <float.h>
#include <cmath>
-#include "../../plugins/mumble_plugin.h"
-#include "Global.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../../plugins/mumble_legacy_plugin.h"
static QPointer< Manual > mDlg = nullptr;
static bool bLinkable = false;
@@ -43,7 +45,7 @@ Manual::Manual(QWidget *p) : QDialog(p) {
qgvPosition->viewport()->installEventFilter(this);
qgvPosition->scale(1.0f, 1.0f);
- qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);
+ m_qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);
const float indicatorDiameter = 4.0f;
QPainterPath indicator;
@@ -53,9 +55,9 @@ Manual::Manual(QWidget *p) : QDialog(p) {
indicator.moveTo(0, indicatorDiameter / 2);
indicator.lineTo(0, indicatorDiameter);
- qgiPosition = qgsScene->addPath(indicator);
+ m_qgiPosition = m_qgsScene->addPath(indicator);
- qgvPosition->setScene(qgsScene);
+ qgvPosition->setScene(m_qgsScene);
qgvPosition->fitInView(-5.0f, -5.0f, 10.0f, 10.0f, Qt::KeepAspectRatio);
qdsbX->setRange(-FLT_MAX, FLT_MAX);
@@ -101,7 +103,7 @@ bool Manual::eventFilter(QObject *obj, QEvent *evt) {
QPointF qpf = qgvPosition->mapToScene(qme->pos());
qdsbX->setValue(qpf.x());
qdsbZ->setValue(-qpf.y());
- qgiPosition->setPos(qpf);
+ m_qgiPosition->setPos(qpf);
}
}
}
@@ -134,8 +136,8 @@ void Manual::on_qpbActivated_clicked(bool b) {
}
void Manual::on_qdsbX_valueChanged(double d) {
- my.avatar_pos[0] = my.camera_pos[0] = static_cast< float >(d);
- qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
+ my.avatar_pos[0] = my.camera_pos[0] = static_cast<float>(d);
+ m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qdsbY_valueChanged(double d) {
@@ -143,8 +145,8 @@ void Manual::on_qdsbY_valueChanged(double d) {
}
void Manual::on_qdsbZ_valueChanged(double d) {
- my.avatar_pos[2] = my.camera_pos[2] = static_cast< float >(d);
- qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
+ my.avatar_pos[2] = my.camera_pos[2] = static_cast<float>(d);
+ m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qsbAzimuth_valueChanged(int i) {
@@ -262,7 +264,7 @@ void Manual::on_speakerPositionUpdate(QHash< unsigned int, Position2D > position
remainingIt.next();
const float speakerRadius = 1.2;
- QGraphicsItem *speakerItem = qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
+ QGraphicsItem *speakerItem = m_qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
2 * speakerRadius, QPen(), QBrush(Qt::red));
Position2D pos = remainingIt.value();
@@ -317,7 +319,7 @@ void Manual::updateTopAndFront(int azimuth, int elevation) {
iAzimuth = azimuth;
iElevation = elevation;
- qgiPosition->setRotation(azimuth);
+ m_qgiPosition->setRotation(azimuth);
double azim = azimuth * M_PI / 180.;
double elev = elevation * M_PI / 180.;
@@ -415,3 +417,16 @@ MumblePlugin *ManualPlugin_getMumblePlugin() {
MumblePluginQt *ManualPlugin_getMumblePluginQt() {
return &manualqt;
}
+
+
+/////////// Implementation of the ManualPlugin class //////////////
+ManualPlugin::ManualPlugin(QObject *p) : LegacyPlugin(QString::fromLatin1("manual.builtin"), true, p) {
+}
+
+ManualPlugin::~ManualPlugin() {
+}
+
+void ManualPlugin::resolveFunctionPointers() {
+ m_mumPlug = &manual;
+ m_mumPlugQt = &manualqt;
+}
diff --git a/src/mumble/ManualPlugin.h b/src/mumble/ManualPlugin.h
index fa76efbc8..dbeee0d31 100644
--- a/src/mumble/ManualPlugin.h
+++ b/src/mumble/ManualPlugin.h
@@ -12,8 +12,7 @@
#include <QtWidgets/QGraphicsScene>
#include "ui_ManualPlugin.h"
-
-#include "../../plugins/mumble_plugin.h"
+#include "LegacyPlugin.h"
#include <atomic>
#include <chrono>
@@ -67,8 +66,8 @@ public slots:
void on_updateStaleSpeakers();
protected:
- QGraphicsScene *qgsScene;
- QGraphicsItem *qgiPosition;
+ QGraphicsScene *m_qgsScene;
+ QGraphicsItem *m_qgiPosition;
std::atomic< bool > updateLoopRunning;
@@ -83,4 +82,20 @@ protected:
MumblePlugin *ManualPlugin_getMumblePlugin();
MumblePluginQt *ManualPlugin_getMumblePluginQt();
+
+/// A built-in "plugin" for positional data gatherig allowing for manually placing the "players" in a UI
+class ManualPlugin : public LegacyPlugin {
+ friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize()
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(ManualPlugin)
+
+ protected:
+ virtual void resolveFunctionPointers() Q_DECL_OVERRIDE;
+ ManualPlugin(QObject *p = nullptr);
+
+ public:
+ virtual ~ManualPlugin() Q_DECL_OVERRIDE;
+};
+
#endif
diff --git a/src/mumble/Messages.cpp b/src/mumble/Messages.cpp
index 62887c34f..729085f5e 100644
--- a/src/mumble/Messages.cpp
+++ b/src/mumble/Messages.cpp
@@ -24,7 +24,6 @@
# include "Overlay.h"
#endif
#include "ChannelListener.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#include "TalkingUI.h"
#include "User.h"
@@ -35,6 +34,7 @@
#include "VersionCheck.h"
#include "ViewCert.h"
#include "crypto/CryptState.h"
+#include "PluginManager.h"
#include "Global.h"
#include <QTextDocumentFragment>
@@ -1303,6 +1303,25 @@ void MainWindow::msgSuggestConfig(const MumbleProto::SuggestConfig &msg) {
}
}
+void MainWindow::msgPluginDataTransmission(const MumbleProto::PluginDataTransmission &msg) {
+ // Another client's plugin has sent us some data. Verify the necessary parts are there and delegate it to the
+ // PluginManager
+
+ if (!msg.has_sendersession() || !msg.has_data() || !msg.has_dataid()) {
+ // if the message contains no sender session, no data or no ID for the data, it is of no use to us and we discard it
+ return;
+ }
+
+ const ClientUser *sender = ClientUser::get(msg.sendersession());
+ const std::string &data = msg.data();
+
+ if (sender) {
+ static_assert(sizeof(unsigned char) == sizeof(uint8_t), "Unsigned char does not have expected 8bit size");
+ // As long as above assertion is true, we are only casting away the sign, which is fine
+ Global::get().pluginManager->on_receiveData(sender, reinterpret_cast< const uint8_t * >(data.c_str()), data.size(), msg.dataid().c_str());
+ }
+}
+
#undef ACTOR_INIT
#undef VICTIM_INIT
#undef SELF_INIT
diff --git a/src/mumble/NetworkConfig.cpp b/src/mumble/NetworkConfig.cpp
index 82cd5c9f4..6b75bd644 100644
--- a/src/mumble/NetworkConfig.cpp
+++ b/src/mumble/NetworkConfig.cpp
@@ -28,6 +28,7 @@ static ConfigRegistrar registrarNetworkConfig(1300, NetworkConfigNew);
NetworkConfig::NetworkConfig(Settings &st) : ConfigWidget(st) {
setupUi(this);
+
qcbType->setAccessibleName(tr("Type"));
qleHostname->setAccessibleName(tr("Hostname"));
qlePort->setAccessibleName(tr("Port"));
@@ -72,7 +73,8 @@ void NetworkConfig::load(const Settings &r) {
const QSignalBlocker blocker(qcbAutoUpdate);
loadCheckBox(qcbAutoUpdate, r.bUpdateCheck);
- loadCheckBox(qcbPluginUpdate, r.bPluginCheck);
+ loadCheckBox(qcbPluginUpdateCheck, r.bPluginCheck);
+ loadCheckBox(qcbPluginAutoUpdate, r.bPluginAutoUpdate);
loadCheckBox(qcbUsage, r.bUsage);
}
@@ -91,9 +93,10 @@ void NetworkConfig::save() const {
s.qsProxyUsername = qleUsername->text();
s.qsProxyPassword = qlePassword->text();
- s.bUpdateCheck = qcbAutoUpdate->isChecked();
- s.bPluginCheck = qcbPluginUpdate->isChecked();
- s.bUsage = qcbUsage->isChecked();
+ s.bUpdateCheck = qcbAutoUpdate->isChecked();
+ s.bPluginCheck = qcbPluginUpdateCheck->isChecked();
+ s.bPluginAutoUpdate = qcbPluginAutoUpdate->isChecked();
+ s.bUsage = qcbUsage->isChecked();
}
static QNetworkProxy::ProxyType local_to_qt_proxy(Settings::ProxyType pt) {
diff --git a/src/mumble/NetworkConfig.ui b/src/mumble/NetworkConfig.ui
index f9c291288..ef783a987 100644
--- a/src/mumble/NetworkConfig.ui
+++ b/src/mumble/NetworkConfig.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>576</width>
- <height>572</height>
+ <height>584</height>
</rect>
</property>
<property name="windowTitle">
@@ -315,7 +315,7 @@ Prevents the client from sending potentially identifying information about the o
</widget>
</item>
<item>
- <widget class="QCheckBox" name="qcbPluginUpdate">
+ <widget class="QCheckBox" name="qcbPluginUpdateCheck">
<property name="toolTip">
<string>Check for new releases of plugins automatically.</string>
</property>
@@ -323,7 +323,14 @@ Prevents the client from sending potentially identifying information about the o
<string>This will check for new releases of plugins every time you start the program, and download them automatically.</string>
</property>
<property name="text">
- <string>Download plugin and overlay updates on startup</string>
+ <string>Check for plugin updates on startup</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbPluginAutoUpdate">
+ <property name="text">
+ <string>Automatically download and install plugin updates</string>
</property>
</widget>
</item>
diff --git a/src/mumble/Plugin.cpp b/src/mumble/Plugin.cpp
new file mode 100644
index 000000000..8317c584f
--- /dev/null
+++ b/src/mumble/Plugin.cpp
@@ -0,0 +1,694 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "Plugin.h"
+#include "Version.h"
+#include "API.h"
+
+#include <QWriteLocker>
+#include <QMutexLocker>
+
+#include <cstring>
+
+
+// initialize the static ID counter
+plugin_id_t Plugin::s_nextID = 1;
+QMutex Plugin::s_idLock(QMutex::NonRecursive);
+
+void assertPluginLoaded(const Plugin* plugin) {
+ // don't throw and exception in release build
+ if (!plugin->isLoaded()) {
+#ifdef QT_DEBUG
+ throw std::runtime_error("Attempting to access plugin but it is not loaded!");
+#else
+ qWarning("Plugin assertion failed: Assumed plugin with ID %d to be loaded but it wasn't!", plugin->getID());
+#endif
+ }
+}
+
+Plugin::Plugin(QString path, bool isBuiltIn, QObject *p)
+ : QObject(p),
+ m_lib(path),
+ m_pluginPath(path),
+ m_pluginIsLoaded(false),
+ m_pluginLock(QReadWriteLock::NonRecursive),
+ m_pluginFnc(),
+ m_isBuiltIn(isBuiltIn),
+ m_positionalDataIsEnabled(true),
+ m_positionalDataIsActive(false),
+ m_mayMonitorKeyboard(false) {
+ // See if the plugin is loadable in the first place unless it is a built-in plugin
+ m_pluginIsValid = isBuiltIn || m_lib.load();
+
+ if (!m_pluginIsValid) {
+ // throw an exception to indicate that the plugin isn't valid
+ throw PluginError("Unable to load the specified library");
+ }
+
+ // aquire id-lock in order to assign an ID to this plugin
+ QMutexLocker lock(&Plugin::s_idLock);
+ m_pluginID = Plugin::s_nextID;
+ Plugin::s_nextID++;
+}
+
+Plugin::~Plugin() {
+ if (isLoaded()) {
+ shutdown();
+ }
+ if (m_lib.isLoaded()) {
+ m_lib.unload();
+ }
+}
+
+QString Plugin::extractWrappedString(MumbleStringWrapper wrapper) const {
+ QString wrappedString = QString::fromUtf8(wrapper.data, wrapper.size);
+
+ if (wrapper.needsReleasing) {
+ releaseResource(static_cast<const void *>(wrapper.data));
+ }
+
+ return wrappedString;
+}
+
+bool Plugin::doInitialize() {
+ resolveFunctionPointers();
+
+ return m_pluginIsValid;
+}
+
+void Plugin::resolveFunctionPointers() {
+ if (isValid()) {
+ // The corresponding library was loaded -> try to locate all API functions and provide defaults for
+ // the missing ones
+
+ QWriteLocker lock(&m_pluginLock);
+
+ // resolve the mandatory functions first
+ m_pluginFnc.init = reinterpret_cast<decltype(MumblePluginFunctions::init)>(m_lib.resolve("mumble_init"));
+ m_pluginFnc.shutdown = reinterpret_cast<decltype(MumblePluginFunctions::shutdown)>(m_lib.resolve("mumble_shutdown"));
+ m_pluginFnc.getName = reinterpret_cast<decltype(MumblePluginFunctions::getName)>(m_lib.resolve("mumble_getName"));
+ m_pluginFnc.getAPIVersion = reinterpret_cast<decltype(MumblePluginFunctions::getAPIVersion)>(m_lib.resolve("mumble_getAPIVersion"));
+ m_pluginFnc.registerAPIFunctions = reinterpret_cast<decltype(MumblePluginFunctions::registerAPIFunctions)>(m_lib.resolve("mumble_registerAPIFunctions"));
+ m_pluginFnc.releaseResource = reinterpret_cast<decltype(MumblePluginFunctions::releaseResource)>(m_lib.resolve("mumble_releaseResource"));
+
+ // validate that all those functions are available in the loaded lib
+ m_pluginIsValid = m_pluginFnc.init && m_pluginFnc.shutdown && m_pluginFnc.getName && m_pluginFnc.getAPIVersion
+ && m_pluginFnc.registerAPIFunctions && m_pluginFnc.releaseResource;
+
+ if (!m_pluginIsValid) {
+ // Don't bother trying to resolve any other functions
+#ifdef MUMBLE_PLUGIN_DEBUG
+#define CHECK_AND_LOG(name) if (!m_pluginFnc.name) { qDebug("\t\"%s\" is missing the %s() function", qPrintable(m_pluginPath), "mumble_" #name); }
+ CHECK_AND_LOG(init);
+ CHECK_AND_LOG(shutdown);
+ CHECK_AND_LOG(getName);
+ CHECK_AND_LOG(getAPIVersion);
+ CHECK_AND_LOG(registerAPIFunctions);
+ CHECK_AND_LOG(releaseResource);
+#undef CHECK_AND_LOG
+#endif
+
+ return;
+ }
+
+ // The mandatory functions are there, now see if any optional functions are implemented as well
+ m_pluginFnc.setMumbleInfo = reinterpret_cast<decltype(MumblePluginFunctions::setMumbleInfo)>(m_lib.resolve("mumble_setMumbleInfo"));
+ m_pluginFnc.getVersion = reinterpret_cast<decltype(MumblePluginFunctions::getVersion)>(m_lib.resolve("mumble_getVersion"));
+ m_pluginFnc.getAuthor = reinterpret_cast<decltype(MumblePluginFunctions::getAuthor)>(m_lib.resolve("mumble_getAuthor"));
+ m_pluginFnc.getDescription = reinterpret_cast<decltype(MumblePluginFunctions::getDescription)>(m_lib.resolve("mumble_getDescription"));
+ m_pluginFnc.getFeatures = reinterpret_cast<decltype(MumblePluginFunctions::getFeatures)>(m_lib.resolve("mumble_getFeatures"));
+ m_pluginFnc.deactivateFeatures = reinterpret_cast<decltype(MumblePluginFunctions::deactivateFeatures)>(m_lib.resolve("mumble_deactivateFeatures"));
+ m_pluginFnc.initPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::initPositionalData)>(m_lib.resolve("mumble_initPositionalData"));
+ m_pluginFnc.fetchPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::fetchPositionalData)>(m_lib.resolve("mumble_fetchPositionalData"));
+ m_pluginFnc.shutdownPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::shutdownPositionalData)>(m_lib.resolve("mumble_shutdownPositionalData"));
+ m_pluginFnc.onServerConnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerConnected)>(m_lib.resolve("mumble_onServerConnected"));
+ m_pluginFnc.onServerDisconnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerDisconnected)>(m_lib.resolve("mumble_onServerDisconnected"));
+ m_pluginFnc.onChannelEntered = reinterpret_cast<decltype(MumblePluginFunctions::onChannelEntered)>(m_lib.resolve("mumble_onChannelEntered"));
+ m_pluginFnc.onChannelExited = reinterpret_cast<decltype(MumblePluginFunctions::onChannelExited)>(m_lib.resolve("mumble_onChannelExited"));
+ m_pluginFnc.onUserTalkingStateChanged = reinterpret_cast<decltype(MumblePluginFunctions::onUserTalkingStateChanged)>(m_lib.resolve("mumble_onUserTalkingStateChanged"));
+ m_pluginFnc.onReceiveData = reinterpret_cast<decltype(MumblePluginFunctions::onReceiveData)>(m_lib.resolve("mumble_onReceiveData"));
+ m_pluginFnc.onAudioInput = reinterpret_cast<decltype(MumblePluginFunctions::onAudioInput)>(m_lib.resolve("mumble_onAudioInput"));
+ m_pluginFnc.onAudioSourceFetched = reinterpret_cast<decltype(MumblePluginFunctions::onAudioSourceFetched)>(m_lib.resolve("mumble_onAudioSourceFetched"));
+ m_pluginFnc.onAudioOutputAboutToPlay = reinterpret_cast<decltype(MumblePluginFunctions::onAudioOutputAboutToPlay)>(m_lib.resolve("mumble_onAudioOutputAboutToPlay"));
+ m_pluginFnc.onServerSynchronized = reinterpret_cast<decltype(MumblePluginFunctions::onServerSynchronized)>(m_lib.resolve("mumble_onServerSynchronized"));
+ m_pluginFnc.onUserAdded = reinterpret_cast<decltype(MumblePluginFunctions::onUserAdded)>(m_lib.resolve("mumble_onUserAdded"));
+ m_pluginFnc.onUserRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onUserRemoved)>(m_lib.resolve("mumble_onUserRemoved"));
+ m_pluginFnc.onChannelAdded = reinterpret_cast<decltype(MumblePluginFunctions::onChannelAdded)>(m_lib.resolve("mumble_onChannelAdded"));
+ m_pluginFnc.onChannelRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRemoved)>(m_lib.resolve("mumble_onChannelRemoved"));
+ m_pluginFnc.onChannelRenamed = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRenamed)>(m_lib.resolve("mumble_onChannelRenamed"));
+ m_pluginFnc.onKeyEvent = reinterpret_cast<decltype(MumblePluginFunctions::onKeyEvent)>(m_lib.resolve("mumble_onKeyEvent"));
+ m_pluginFnc.hasUpdate = reinterpret_cast<decltype(MumblePluginFunctions::hasUpdate)>(m_lib.resolve("mumble_hasUpdate"));
+ m_pluginFnc.getUpdateDownloadURL = reinterpret_cast<decltype(MumblePluginFunctions::getUpdateDownloadURL)>(m_lib.resolve("mumble_getUpdateDownloadURL"));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+#define CHECK_AND_LOG(name) qDebug("\t" "mumble_" #name ": %s", (m_pluginFnc.name == nullptr ? "no" : "yes"))
+ qDebug(">>>> Found optional functions for plugin \"%s\"", qUtf8Printable(m_pluginPath));
+ CHECK_AND_LOG(setMumbleInfo);
+ CHECK_AND_LOG(getVersion);
+ CHECK_AND_LOG(getAuthor);
+ CHECK_AND_LOG(getDescription);
+ CHECK_AND_LOG(getFeatures);
+ CHECK_AND_LOG(deactivateFeatures);
+ CHECK_AND_LOG(initPositionalData);
+ CHECK_AND_LOG(fetchPositionalData);
+ CHECK_AND_LOG(shutdownPositionalData);
+ CHECK_AND_LOG(onServerConnected);
+ CHECK_AND_LOG(onServerDisconnected);
+ CHECK_AND_LOG(onChannelEntered);
+ CHECK_AND_LOG(onChannelExited);
+ CHECK_AND_LOG(onUserTalkingStateChanged);
+ CHECK_AND_LOG(onReceiveData);
+ CHECK_AND_LOG(onAudioInput);
+ CHECK_AND_LOG(onAudioSourceFetched);
+ CHECK_AND_LOG(onAudioOutputAboutToPlay);
+ CHECK_AND_LOG(onServerSynchronized);
+ CHECK_AND_LOG(onUserAdded);
+ CHECK_AND_LOG(onUserRemoved);
+ CHECK_AND_LOG(onChannelAdded);
+ CHECK_AND_LOG(onChannelRemoved);
+ CHECK_AND_LOG(onChannelRenamed);
+ CHECK_AND_LOG(onKeyEvent);
+ CHECK_AND_LOG(hasUpdate);
+ CHECK_AND_LOG(getUpdateDownloadURL);
+ qDebug("<<<<");
+#endif
+
+ // If positional audio is to be supported, all three corresponding functions have to be implemented
+ // For PA it is all or nothing
+ if (!(m_pluginFnc.initPositionalData && m_pluginFnc.fetchPositionalData && m_pluginFnc.shutdownPositionalData)
+ && (m_pluginFnc.initPositionalData || m_pluginFnc.fetchPositionalData || m_pluginFnc.shutdownPositionalData)) {
+ m_pluginFnc.initPositionalData = nullptr;
+ m_pluginFnc.fetchPositionalData = nullptr;
+ m_pluginFnc.shutdownPositionalData = nullptr;
+#ifdef MUMBLE_PLUGIN_DEBUG
+ qDebug("\t\"%s\" has only partially implemented positional data functions -> deactivating all of them", qPrintable(m_pluginPath));
+#endif
+ }
+ }
+}
+
+bool Plugin::isValid() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginIsValid;
+}
+
+bool Plugin::isLoaded() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginIsLoaded;
+}
+
+plugin_id_t Plugin::getID() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginID;
+}
+
+bool Plugin::isBuiltInPlugin() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_isBuiltIn;
+}
+
+QString Plugin::getFilePath() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginPath;
+}
+
+bool Plugin::isPositionalDataEnabled() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_positionalDataIsEnabled;
+}
+
+void Plugin::enablePositionalData(bool enable) {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_positionalDataIsEnabled = enable;
+}
+
+bool Plugin::isPositionalDataActive() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_positionalDataIsActive;
+}
+
+void Plugin::allowKeyboardMonitoring(bool allow) {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_mayMonitorKeyboard = allow;
+}
+
+bool Plugin::isKeyboardMonitoringAllowed() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_mayMonitorKeyboard;
+}
+
+mumble_error_t Plugin::init() {
+ {
+ QReadLocker lock(&m_pluginLock);
+
+ if (m_pluginIsLoaded) {
+ return STATUS_OK;
+ }
+ }
+
+ //////////////////////////////
+ // Step 1: Introduce ourselves (inform the plugin about Mumble's (API) version
+
+ // Get Mumble version
+ int mumbleMajor, mumbleMinor, mumblePatch;
+ MumbleVersion::get(&mumbleMajor, &mumbleMinor, &mumblePatch);
+
+ // Require API version 1.0.0 as the minimal supported one
+ setMumbleInfo({ mumbleMajor, mumbleMinor, mumblePatch }, MUMBLE_PLUGIN_API_VERSION, { 1, 0, 0 });
+
+
+ //////////////////////////////
+ // Step 2: Provide the API functions to the plugin
+ const mumble_version_t apiVersion = getAPIVersion();
+ if (apiVersion >= mumble_version_t({1, 0, 0}) && apiVersion < mumble_version_t({1, 2, 0})) {
+ MumbleAPI_v_1_0_x api = API::getMumbleAPI_v_1_0_x();
+ registerAPIFunctions(&api);
+ } else {
+ // The API version could not be obtained -> this is an invalid plugin that shouldn't have been loaded in the first place
+ qWarning("Unable to obtain requested MumbleAPI version");
+ return EC_INVALID_API_VERSION;
+ }
+
+
+ //////////////////////////////
+ // Step 3: Actually try to load the plugin
+
+ mumble_error_t retStatus;
+ if (m_pluginFnc.init) {
+ retStatus = m_pluginFnc.init(m_pluginID);
+ } else {
+ retStatus = EC_GENERIC_ERROR;
+ }
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+ m_pluginIsLoaded = retStatus == STATUS_OK;
+ }
+
+ return retStatus;
+}
+
+void Plugin::shutdown() {
+ bool posDataActive;
+ {
+ QReadLocker rLock(&m_pluginLock);
+ if (!m_pluginIsLoaded) {
+ return;
+ }
+
+ posDataActive = m_positionalDataIsActive;
+ }
+
+ if (posDataActive) {
+ shutdownPositionalData();
+ }
+
+ if (m_pluginFnc.shutdown) {
+ m_pluginFnc.shutdown();
+ }
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_pluginIsLoaded = false;
+ }
+}
+
+QString Plugin::getName() const {
+ if (m_pluginFnc.getName) {
+ return extractWrappedString(m_pluginFnc.getName());
+ } else {
+ return QString::fromLatin1("Unknown plugin");
+ }
+}
+
+mumble_version_t Plugin::getAPIVersion() const {
+ if (m_pluginFnc.getAPIVersion) {
+ return m_pluginFnc.getAPIVersion();
+ } else {
+ return VERSION_UNKNOWN;
+ }
+}
+
+void Plugin::registerAPIFunctions(void *api) const {
+ if (m_pluginFnc.registerAPIFunctions) {
+ m_pluginFnc.registerAPIFunctions(api);
+ }
+}
+
+void Plugin::releaseResource(const void *pointer) const {
+ if (m_pluginFnc.releaseResource) {
+ m_pluginFnc.releaseResource(pointer);
+ }
+}
+
+void Plugin::setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const {
+ if (m_pluginFnc.setMumbleInfo) {
+ m_pluginFnc.setMumbleInfo(mumbleVersion, mumbleAPIVersion, minimalExpectedAPIVersion);
+ }
+}
+
+mumble_version_t Plugin::getVersion() const {
+ if (m_pluginFnc.getVersion) {
+ return m_pluginFnc.getVersion();
+ } else {
+ return VERSION_UNKNOWN;
+ }
+}
+
+QString Plugin::getAuthor() const {
+ if (m_pluginFnc.getAuthor) {
+ return extractWrappedString(m_pluginFnc.getAuthor());
+ } else {
+ return QString::fromLatin1("Unknown");
+ }
+}
+
+QString Plugin::getDescription() const {
+ if (m_pluginFnc.getDescription) {
+ return extractWrappedString(m_pluginFnc.getDescription());
+ } else {
+ return QString::fromLatin1("No description provided");
+ }
+}
+
+uint32_t Plugin::getFeatures() const {
+ if (m_pluginFnc.getFeatures) {
+ return m_pluginFnc.getFeatures();
+ } else {
+ return FEATURE_NONE;
+ }
+}
+
+uint32_t Plugin::deactivateFeatures(uint32_t features) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.deactivateFeatures) {
+ return m_pluginFnc.deactivateFeatures(features);
+ } else {
+ return features;
+ }
+}
+
+bool Plugin::showAboutDialog(QWidget *parent) const {
+ assertPluginLoaded(this);
+
+ Q_UNUSED(parent);
+ return false;
+}
+
+bool Plugin::showConfigDialog(QWidget *parent) const {
+ assertPluginLoaded(this);
+
+ Q_UNUSED(parent);
+ return false;
+}
+
+uint8_t Plugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.initPositionalData) {
+ uint8_t returnCode = m_pluginFnc.initPositionalData(programNames, programPIDs, programCount);
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+ m_positionalDataIsActive = returnCode == PDEC_OK;
+ }
+
+ return returnCode;
+ } else {
+ return PDEC_ERROR_PERM;
+ }
+}
+
+bool Plugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.fetchPositionalData) {
+ const char *contextPtr = "";
+ const char *identityPtr = "";
+
+ bool retStatus = m_pluginFnc.fetchPositionalData(static_cast<float*>(avatarPos), static_cast<float*>(avatarDir),
+ static_cast<float*>(avatarAxis), static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(cameraAxis),
+ &contextPtr, &identityPtr);
+
+ context = QString::fromUtf8(contextPtr);
+ identity = QString::fromUtf8(identityPtr);
+
+ return retStatus;
+ } else {
+ avatarPos.toZero();
+ avatarDir.toZero();
+ avatarAxis.toZero();
+ cameraPos.toZero();
+ cameraDir.toZero();
+ cameraAxis.toZero();
+ context = QString();
+ identity = QString();
+
+ return false;
+ }
+}
+
+void Plugin::shutdownPositionalData() {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.shutdownPositionalData) {
+ m_positionalDataIsActive = false;
+
+ m_pluginFnc.shutdownPositionalData();
+ }
+}
+
+void Plugin::onServerConnected(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerConnected) {
+ m_pluginFnc.onServerConnected(connection);
+ }
+}
+
+void Plugin::onServerDisconnected(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerDisconnected) {
+ m_pluginFnc.onServerDisconnected(connection);
+ }
+}
+
+void Plugin::onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID,
+ mumble_channelid_t newChannelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelEntered) {
+ m_pluginFnc.onChannelEntered(connection, userID, previousChannelID, newChannelID);
+ }
+}
+
+void Plugin::onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelExited) {
+ m_pluginFnc.onChannelExited(connection, userID, channelID);
+ }
+}
+
+void Plugin::onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserTalkingStateChanged) {
+ m_pluginFnc.onUserTalkingStateChanged(connection, userID, talkingState);
+ }
+}
+
+bool Plugin::onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onReceiveData) {
+ return m_pluginFnc.onReceiveData(connection, sender, data, dataLength, dataID);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioInput) {
+ return m_pluginFnc.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioSourceFetched) {
+ return m_pluginFnc.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, userID);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioOutputAboutToPlay) {
+ return m_pluginFnc.onAudioOutputAboutToPlay(outputPCM, sampleCount, channelCount, sampleRate);
+ } else {
+ return false;
+ }
+}
+
+void Plugin::onServerSynchronized(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerSynchronized) {
+ m_pluginFnc.onServerSynchronized(connection);
+ }
+}
+
+void Plugin::onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserAdded) {
+ m_pluginFnc.onUserAdded(connection, userID);
+ }
+}
+
+void Plugin::onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserRemoved) {
+ m_pluginFnc.onUserRemoved(connection, userID);
+ }
+}
+
+void Plugin::onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelAdded) {
+ m_pluginFnc.onChannelAdded(connection, channelID);
+ }
+}
+
+void Plugin::onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelRemoved) {
+ m_pluginFnc.onChannelRemoved(connection, channelID);
+ }
+}
+
+void Plugin::onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelRenamed) {
+ m_pluginFnc.onChannelRenamed(connection, channelID);
+ }
+}
+
+void Plugin::onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const {
+ assertPluginLoaded(this);
+
+ if (!m_mayMonitorKeyboard) {
+ // Keyboard monitoring is forbidden for this plugin
+ return;
+ }
+
+ if (m_pluginFnc.onKeyEvent) {
+ m_pluginFnc.onKeyEvent(keyCode, wasPress);
+ }
+}
+
+bool Plugin::hasUpdate() const {
+ if (m_pluginFnc.hasUpdate) {
+ return m_pluginFnc.hasUpdate();
+ } else {
+ // A plugin that doesn't implement this function is assumed to never know about
+ // any potential updates
+ return false;
+ }
+}
+
+QUrl Plugin::getUpdateDownloadURL() const {
+ if (m_pluginFnc.getUpdateDownloadURL) {
+ return QUrl(extractWrappedString(m_pluginFnc.getUpdateDownloadURL()));
+ } else {
+ // Return an empty URL as a fallback
+ return QUrl();
+ }
+}
+
+bool Plugin::providesAboutDialog() const {
+ return false;
+}
+
+bool Plugin::providesConfigDialog() const {
+ return false;
+}
+
+
+
+/////////////////// Implementation of the PluginReadLocker /////////////////////////
+PluginReadLocker::PluginReadLocker(QReadWriteLock *lock)
+ : m_lock(lock),
+ m_unlocked(false) {
+ relock();
+}
+
+void PluginReadLocker::unlock() {
+ if (!m_lock) {
+ // do nothgin for nullptr
+ return;
+ }
+
+ m_unlocked = true;
+
+ m_lock->unlock();
+}
+
+void PluginReadLocker::relock() {
+ if (!m_lock) {
+ // do nothing for a nullptr
+ return;
+ }
+
+ // First try to lock for read-access
+ if (!m_lock->tryLockForRead()) {
+ // if that fails, we'll try to lock for write-access
+ // That will only succeed in the case that the current thread holds the write-access to this lock already which caused
+ // the previous attempt to lock for reading to fail (by design of the QtReadWriteLock).
+ // As we are in the thread with the write-access, it means that this threads has asked for read-access on top of it which we will
+ // grant (in contrast of QtReadLocker) because if you have the permission to change something you surely should have permission
+ // to read it. This assumes that the thread won't try to read data it temporarily has corrupted.
+ if (!m_lock->tryLockForWrite()) {
+ // If we couldn't lock for write at this point, it means another thread has write-access granted by the lock so we'll have to wait
+ // in order to gain regular read-access as would be with QtReadLocker
+ m_lock->lockForRead();
+ }
+ }
+
+ m_unlocked = false;
+}
+
+PluginReadLocker::~PluginReadLocker() {
+ if (m_lock && !m_unlocked) {
+ // unlock the lock if it isn't nullptr
+ m_lock->unlock();
+ }
+}
diff --git a/src/mumble/Plugin.h b/src/mumble/Plugin.h
new file mode 100644
index 000000000..049e622f2
--- /dev/null
+++ b/src/mumble/Plugin.h
@@ -0,0 +1,417 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGIN_H_
+#define MUMBLE_MUMBLE_PLUGIN_H_
+
+#include "MumbleAPI_v_1_0_x.h"
+#include "PluginComponents_v_1_0_x.h"
+#include "PositionalData.h"
+#include "MumblePlugin_v_1_0_x.h"
+
+#include <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QLibrary>
+#include <QMutex>
+#include <QUrl>
+
+#include <stdexcept>
+#include <memory>
+
+/// A struct for holding the function pointers to the functions inside the plugin's library
+/// For the documentation of those functions, see the plugin's header file (the one used when developing a plugin)
+struct MumblePluginFunctions {
+ decltype(&mumble_init) init;
+ decltype(&mumble_shutdown) shutdown;
+ decltype(&mumble_getName) getName;
+ decltype(&mumble_getAPIVersion) getAPIVersion;
+ decltype(&mumble_registerAPIFunctions) registerAPIFunctions;
+ decltype(&mumble_releaseResource) releaseResource;
+
+ // Further utility functions the plugin may implement
+ decltype(&mumble_setMumbleInfo) setMumbleInfo;
+ decltype(&mumble_getVersion) getVersion;
+ decltype(&mumble_getAuthor) getAuthor;
+ decltype(&mumble_getDescription) getDescription;
+ decltype(&mumble_getFeatures) getFeatures;
+ decltype(&mumble_deactivateFeatures) deactivateFeatures;
+
+ // Functions for dealing with positional audio (or rather the fetching of the needed data)
+ decltype(&mumble_initPositionalData) initPositionalData;
+ decltype(&mumble_fetchPositionalData) fetchPositionalData;
+ decltype(&mumble_shutdownPositionalData) shutdownPositionalData;
+
+ // Callback functions and EventHandlers
+ decltype(&mumble_onServerConnected) onServerConnected;
+ decltype(&mumble_onServerDisconnected) onServerDisconnected;
+ decltype(&mumble_onChannelEntered) onChannelEntered;
+ decltype(&mumble_onChannelExited) onChannelExited;
+ decltype(&mumble_onUserTalkingStateChanged) onUserTalkingStateChanged;
+ decltype(&mumble_onReceiveData) onReceiveData;
+ decltype(&mumble_onAudioInput) onAudioInput;
+ decltype(&mumble_onAudioSourceFetched) onAudioSourceFetched;
+ decltype(&mumble_onAudioOutputAboutToPlay) onAudioOutputAboutToPlay;
+ decltype(&mumble_onServerSynchronized) onServerSynchronized;
+ decltype(&mumble_onUserAdded) onUserAdded;
+ decltype(&mumble_onUserRemoved) onUserRemoved;
+ decltype(&mumble_onChannelAdded) onChannelAdded;
+ decltype(&mumble_onChannelRemoved) onChannelRemoved;
+ decltype(&mumble_onChannelRenamed) onChannelRenamed;
+ decltype(&mumble_onKeyEvent) onKeyEvent;
+
+ // Plugin updates
+ decltype(&mumble_hasUpdate) hasUpdate;
+ decltype(&mumble_getUpdateDownloadURL) getUpdateDownloadURL;
+};
+
+
+/// An exception that is being thrown by a plugin whenever it encounters an error
+class PluginError : public std::runtime_error {
+ public:
+ // inherit constructors of runtime_error
+ using std::runtime_error::runtime_error;
+};
+
+
+/// An implementation similar to QReadLocker except that this one allows to lock on a lock the same thread is already
+/// holding a write-lock on. This could also result in obtaining a write-lock though so it shouldn't be used for code regions
+/// that take quite some time and rely on other readers still having access to the locked object.
+class PluginReadLocker {
+ protected:
+ /// The lock this lock-guard is acting upon
+ QReadWriteLock *m_lock;
+ /// A flag indicating whether the lock has been unlocked (manually) and thus doesn't have to be unlocked
+ /// in the destructor.
+ bool m_unlocked;
+ public:
+ /// Constructor of the PluginReadLocker. If the passed lock-pointer is not nullptr, the constructor will
+ /// already lock the provided lock.
+ ///
+ /// @param lock A pointer to the QReadWriteLock that shall be managed by this object. May be nullptr
+ PluginReadLocker(QReadWriteLock *lock);
+ /// Locks this lock again after it has been unlocked before (Locking a locked lock results in a runtime error)
+ void relock();
+ /// Unlocks this lock
+ void unlock();
+ ~PluginReadLocker();
+};
+
+class Plugin;
+
+/// Typedef for the plugin ID
+typedef uint32_t plugin_id_t;
+/// Typedef for a plugin pointer
+typedef std::shared_ptr<Plugin> plugin_ptr_t;
+/// Typedef for a const plugin pointer
+typedef std::shared_ptr<const Plugin> const_plugin_ptr_t;
+
+/// A class representing a plugin library attached to Mumble. It can be used to manage (load/unload) and access plugin libraries.
+class Plugin : public QObject {
+ friend class PluginManager;
+ friend class PluginConfig;
+
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(Plugin)
+ protected:
+ /// A mutex guarding Plugin::nextID
+ static QMutex s_idLock;
+ /// The ID of the plugin that will be loaded next. Whenever accessing this field, Plugin::idLock should be locked.
+ static plugin_id_t s_nextID;
+
+ /// Constructor of the Plugin.
+ ///
+ /// @param path The path to the plugin's shared library file. This path has to exist unless isBuiltIn is true
+ /// @param isBuiltIn A flag indicating that this is a plugin built into Mumble itself and is does not backed by a shared library
+ /// @param p A pointer to a QObject representing the parent of this object or nullptr if there is no parent
+ Plugin(QString path, bool isBuiltIn = false, QObject *p = nullptr);
+
+ /// A flag indicating whether this plugin is valid. It is mainly used throughout the plugin's initialization.
+ bool m_pluginIsValid;
+ /// The QLibrary representing the shared library of this plugin
+ QLibrary m_lib;
+ /// The path to the shared library file in the host's filesystem
+ QString m_pluginPath;
+ /// The unique ID of this plugin. Note though that this ID is not suitable for uniquely identifying this plugin between restarts of Mumble
+ /// (not even between rescans of the plugins) let alone across clients.
+ plugin_id_t m_pluginID;
+ // a flag indicating whether this plugin has been loaded by calling its init function.
+ bool m_pluginIsLoaded;
+ /// The lock guarding this plugin object. Every time a member is accessed this lock should be locked accordingly.
+ /// After successful construction and initialization (doInitilize()), this member variable is effectively const
+ /// and therefore no locking is required in order to read from it!
+ /// In fact protecting read-accesses by a non-recursive lock can introduce deadlocks by plugins using certain
+ /// API functions.
+ mutable QReadWriteLock m_pluginLock;
+ /// The struct holding the function pointers to the functions in the shared library.
+ MumblePluginFunctions m_pluginFnc;
+ /// A flag indicating whether this plugin is built into Mumble and is thus not represented by a shared library.
+ bool m_isBuiltIn;
+ /// A flag indicating whether positional data gathering is enabled for this plugin (Enabled as in allowed via preferences).
+ bool m_positionalDataIsEnabled;
+ /// A flag indicating whether positional data gathering is currently active (Active as in running)
+ bool m_positionalDataIsActive;
+ /// A flag indicating whether this plugin has permission to monitor keyboard events that occur while
+ /// Mumble has the keyboard focus.
+ bool m_mayMonitorKeyboard;
+
+
+ QString extractWrappedString(MumbleStringWrapper wrapper) const;
+
+
+ // Most of this class's functions are protected in order to only allow access to them via the PluginManager
+ // as some require additional handling before/after calling them.
+
+ /// Initializes this plugin. This function must be called directly after construction. This is guaranteed when the
+ /// plugin is created via Plugin::createNew
+ virtual bool doInitialize();
+ /// Resolves the function pointers in the shared library and sets the respective fields in Plugin::apiFnc
+ virtual void resolveFunctionPointers();
+ /// Enables positional data gathering for this plugin (as in allowing)
+ ///
+ /// @param enable Whether to enable the data gathering
+ virtual void enablePositionalData(bool enable = true);
+ /// Allows or forbids the monitoring of keyboard events for this plugin.
+ ///
+ /// @param allow Whether to allow or forbid it
+ virtual void allowKeyboardMonitoring(bool allow);
+
+
+ /// Initializes this plugin
+ virtual mumble_error_t init();
+ /// Shuts this plugin down
+ virtual void shutdown();
+ /// Delegates the struct of API function pointers to the plugin backend
+ ///
+ /// @param api The pointer to the API struct
+ virtual void registerAPIFunctions(void *api) const;
+ /// Asks the plugin to release (free/delete) the resource pointed to by the given pointer
+ ///
+ /// @param pointer Pointer to the resource
+ virtual void releaseResource(const void *pointer) const;
+ /// Provides the plugin backend with some version information about Mumble
+ ///
+ /// @param mumbleVersion The version of the Mumble client
+ /// @param mumbleAPIVersion The API version used by the Mumble client
+ /// @param minimalExpectedAPIVersion The minimal API version expected to be used by the plugin backend
+ virtual void setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const;
+ /// Asks the plugin to deactivate certain features
+ ///
+ /// @param features The feature list or'ed together
+ /// @returns The list of features that couldn't be deactivated or'ed together
+ virtual uint32_t deactivateFeatures(uint32_t features) const;
+ /// Shows an about-dialog
+ ///
+ /// @parent A pointer to the QWidget that should be used as a parent
+ /// @returns Whether the dialog could be shown successfully
+ virtual bool showAboutDialog(QWidget *parent) const;
+ /// Shows a config-dialog
+ ///
+ /// @parent A pointer to the QWidget that should be used as a parent
+ /// @returns Whether the dialog could be shown successfully
+ virtual bool showConfigDialog(QWidget *parent) const;
+ /// Initializes the positional data gathering
+ ///
+ /// @params programNames A pointer to an array of const char* representing the program names
+ /// @params programCount A pointer to an array of PIDs corresponding to the program names
+ /// @params programCount The length of the two previous arrays
+ virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount);
+ /// Fetches the positional data
+ ///
+ /// @param[out] avatarPos The position of the ingame avatar (player)
+ /// @param[out] avatarDir The directiion in which the avatar (player) is looking/facing
+ /// @param[out] avatarAxis The vector from the avatar's toes to its head
+ /// @param[out] cameraPos The position of the ingame camera
+ /// @param[out] cameraDir The direction in which the camera is looking/facing
+ /// @param[out] cameraAxis The vector from the camera's bottom to its top
+ /// @param[out] context The context of the current game-session (includes server/squad info)
+ /// @param[out] identity The ingame identity of the player (name)
+ virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const;
+ /// Shuts down positional data gathering
+ virtual void shutdownPositionalData();
+ /// Called to indicate that the client has connected to a server
+ ///
+ /// @param connection An object used to identify the current connection
+ virtual void onServerConnected(mumble_connection_t connection) const;
+ /// Called to indicate that the client disconnected from a server
+ ///
+ /// @param connection An object used to identify the connection that has been disconnected
+ virtual void onServerDisconnected(mumble_connection_t connection) const;
+ /// Called to indicate that a user has switched its channel
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param previousChannelID The ID of the channel the user came from (-1 if there is no previous channel)
+ /// æparam newChannelID The ID of the channel the user has switched to
+ virtual void onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID,
+ mumble_channelid_t newChannelID) const;
+ /// Called to indicate that a user exited a channel.
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param channelID The ID of the channel the user exited
+ virtual void onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const;
+ /// Called to indicate that a user has changed its talking state
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param talkingState The new talking state of the user
+ virtual void onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const;
+ /// Called to indicate that a data packet has been received
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param sender The ID of the user whose client sent the data
+ /// @param data The actual data
+ /// @param dataLength The length of the data array
+ /// @param datID The ID of the data used to determine whether this plugin handles this data or not
+ /// @returns Whether this plugin handled the data
+ virtual bool onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const;
+ /// Called to indicate that there is audio input
+ ///
+ /// @param inputPCM A pointer to a short array representing the input PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the input as speech
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const;
+ /// Called to indicate that an audio source has been fetched
+ ///
+ /// @param outputPCM A pointer to a short array representing the output PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the output as speech
+ /// @param userID The ID of the user responsible for the output (only relevant if isSpeech == true)
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const;
+ /// Called to indicate that audio is about to be played
+ ///
+ /// @param outputPCM A pointer to a short array representing the output PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const;
+ /// Called when the server has synchronized with the client
+ ///
+ /// @param connection An object used to identify the current connection
+ virtual void onServerSynchronized(mumble_connection_t connection) const;
+ /// Called when a new user gets added to the user model. This is the case when that new user freshly connects to the server the
+ /// local user is on but also when the local user connects to a server other clients are already connected to (in this case this
+ /// method will be called for every client already on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been added
+ virtual void onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const;
+ /// Called when a user gets removed from the user model. This is the case when that user disconnects from the server the
+ /// local user is on but also when the local user disconnects from a server other clients are connected to (in this case this
+ /// method will be called for every client on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been removed
+ virtual void onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const;
+ /// Called when a new channel gets added to the user model. This is the case when a new channel is created on the server the local
+ /// user is on but also when the local user connects to a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been added
+ virtual void onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a channel gets removed from the user model. This is the case when a channel is removed on the server the local
+ /// user is on but also when the local user disconnects from a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been removed
+ virtual void onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a channel gets renamed. This also applies when a new channel is created (thus assigning it an initial name is
+ /// also considered renaming).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been renamed
+ virtual void onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a key has been pressed or released while Mumble has keyboard focus.
+ ///
+ /// @param keyCode The key code of the respective key. The character codes are defined
+ /// via the KeyCode enum. For printable 7-bit ASCII characters these codes conform
+ /// to the ASCII code-page with the only difference that case is not distinguished. Therefore
+ /// always the upper-case letter code will be used for letters.
+ /// @param wasPress Whether the key has been pressed (instead of released)
+ virtual void onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const;
+
+
+ public:
+ /// A template function for instantiating new plugin objects and initializing them. The plugin will be allocated on the heap and has
+ /// thus to be deleted via the delete instruction.
+ ///
+ /// @tparam T The type of the plugin to be instantiated
+ /// @tparam Ts The types of the contructor arguments
+ /// @param args A list of args passed to the contructor of the plugin object
+ /// @returns A pointer to the allocated plugin
+ ///
+ /// @throws PluginError if the plugin could not be loaded
+ template<typename T, typename ... Ts>
+ static T* createNew(Ts&&...args) {
+ static_assert(std::is_base_of<Plugin, T>::value, "The Plugin::create() can only be used to instantiate objects of base-type Plugin");
+ static_assert(!std::is_pointer<T>::value, "Plugin::create() can't be used to instantiate pointers. It will return a pointer automatically");
+
+ T *instancePtr = new T(std::forward<Ts>(args)...);
+
+ // call the initialize-method and throw an exception of it doesn't succeed
+ if (!instancePtr->doInitialize()) {
+ delete instancePtr;
+ // Delete the constructed object to prevent a memory leak
+ throw PluginError("Failed to initialize plugin");
+ }
+
+ return instancePtr;
+ }
+
+ /// Destructor
+ virtual ~Plugin() Q_DECL_OVERRIDE;
+ /// @returns Whether this plugin is in a valid state
+ virtual bool isValid() const;
+ /// @returns Whether this plugin is loaded (has been initialized via Plugin::init())
+ virtual bool isLoaded() const Q_DECL_FINAL;
+ /// @returns The unique ID of this plugin. This ID holds only as long as this plugin isn't "reconstructed".
+ virtual plugin_id_t getID() const Q_DECL_FINAL;
+ /// @returns Whether this plugin is built into Mumble (thus not backed by a shared library)
+ virtual bool isBuiltInPlugin() const Q_DECL_FINAL;
+ /// @returns The path to the shared library in the host's filesystem
+ virtual QString getFilePath() const;
+ /// @returns Whether positional data gathering is enabled (as in allowed via preferences)
+ virtual bool isPositionalDataEnabled() const Q_DECL_FINAL;
+ /// @returns Whether positional data gathering is currently active (as in running)
+ virtual bool isPositionalDataActive() const Q_DECL_FINAL;
+ /// @returns Whether this plugin is currently allowed to monitor keyboard events
+ virtual bool isKeyboardMonitoringAllowed() const Q_DECL_FINAL;
+
+
+ /// @returns Whether this plugin provides an about-dialog
+ virtual bool providesAboutDialog() const;
+ /// @returns Whether this plugin provides an config-dialog
+ virtual bool providesConfigDialog() const;
+ /// @returns The name of this plugin
+ virtual QString getName() const;
+ /// @returns The API version this plugin intends to use
+ virtual mumble_version_t getAPIVersion() const;
+ /// @returns The version of this plugin
+ virtual mumble_version_t getVersion() const;
+ /// @returns The author of this plugin
+ virtual QString getAuthor() const;
+ /// @returns The plugin's description
+ virtual QString getDescription() const;
+ /// @returns The plugin's features or'ed together (See the PluginFeature enum in MumblePlugin.h for what features are available)
+ virtual uint32_t getFeatures() const;
+ /// @return Whether the plugin has found a new/updated version of itself available for download
+ virtual bool hasUpdate() const;
+ /// @return The URL to download the updated plugin. May be empty
+ virtual QUrl getUpdateDownloadURL() const;
+};
+
+#endif
diff --git a/src/mumble/PluginConfig.cpp b/src/mumble/PluginConfig.cpp
new file mode 100644
index 000000000..e70877067
--- /dev/null
+++ b/src/mumble/PluginConfig.cpp
@@ -0,0 +1,247 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginConfig.h"
+
+#include "Log.h"
+#include "MainWindow.h"
+#include "Message.h"
+#include "ServerHandler.h"
+#include "WebFetch.h"
+#include "MumbleApplication.h"
+#include "ManualPlugin.h"
+#include "Utils.h"
+#include "PluginInstaller.h"
+#include "PluginManager.h"
+
+#include <QtWidgets/QMessageBox>
+#include <QtCore/QUrl>
+#include <QtCore/QDir>
+#include <QtCore/QStandardPaths>
+#include <QtWidgets/QFileDialog>
+#include "Global.h"
+
+const QString PluginConfig::name = QLatin1String("PluginConfig");
+
+static ConfigWidget *PluginConfigDialogNew(Settings &st) {
+ return new PluginConfig(st);
+}
+
+static ConfigRegistrar registrarPluginConfig(5000, PluginConfigDialogNew);
+
+
+PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) {
+ setupUi(this);
+
+ qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+ qtwPlugins->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ qtwPlugins->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
+
+ qpbUnload->setEnabled(false);
+
+ refillPluginList();
+}
+
+QString PluginConfig::title() const {
+ return tr("Plugins");
+}
+
+const QString &PluginConfig::getName() const {
+ return PluginConfig::name;
+}
+
+QIcon PluginConfig::icon() const {
+ return QIcon(QLatin1String("skin:config_plugin.png"));
+}
+
+void PluginConfig::load(const Settings &r) {
+ loadCheckBox(qcbTransmit, r.bTransmitPosition);
+}
+
+void PluginConfig::on_qpbInstallPlugin_clicked() {
+ QString pluginFile = QFileDialog::getOpenFileName(this, tr("Install plugin..."), QDir::homePath());
+
+ if (pluginFile.isEmpty()) {
+ return;
+ }
+
+ try {
+ PluginInstaller installer(pluginFile, this);
+ if (installer.exec() == QDialog::Accepted) {
+ // Reload plugins so the new one actually shows up
+ on_qpbReload_clicked();
+
+ QMessageBox::information(this, "Mumble", tr("The plugin was installed successfully"), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ } catch (const PluginInstallException &e) {
+ QMessageBox::critical(this, "Mumble", e.getMessage(), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+}
+
+void PluginConfig::save() const {
+ s.bTransmitPosition = qcbTransmit->isChecked();
+ s.qhPluginSettings.clear();
+
+ if (!s.bTransmitPosition) {
+ // Make sure that if posData is currently running, it gets reset
+ // The setting will prevent the system from reactivating
+ Global::get().pluginManager->unlinkPositionalData();
+ }
+
+ constexpr int enableCol = 1;
+ constexpr int positionalDataCol = 2;
+ constexpr int keyboardMonitorCol = 3;
+
+ QList<QTreeWidgetItem *> list = qtwPlugins->findItems(QString(), Qt::MatchContains);
+ for(QTreeWidgetItem *i : list) {
+
+ bool enable = (i->checkState(enableCol) == Qt::Checked);
+ bool positionalDataEnabled = (i->checkState(positionalDataCol) == Qt::Checked);
+ bool keyboardMonitoringEnabled = (i->checkState(keyboardMonitorCol) == Qt::Checked);
+
+ const_plugin_ptr_t plugin = pluginForItem(i);
+ if (plugin) {
+ // insert plugin to settings
+ Global::get().pluginManager->enablePositionalDataFor(plugin->getID(), positionalDataEnabled);
+ Global::get().pluginManager->allowKeyboardMonitoringFor(plugin->getID(), keyboardMonitoringEnabled);
+
+ if (enable) {
+ if (Global::get().pluginManager->loadPlugin(plugin->getID())) {
+ // potentially deactivate plugin features
+ // A plugin's feature is considered to be enabled by default after loading. Thus we only need to
+ // deactivate the ones we don't want
+ uint32_t featuresToDeactivate = FEATURE_NONE;
+ const uint32_t pluginFeatures = plugin->getFeatures();
+
+ if (!positionalDataEnabled && (pluginFeatures & FEATURE_POSITIONAL)) {
+ // deactivate this feature only if it is available in the first place
+ featuresToDeactivate |= FEATURE_POSITIONAL;
+ }
+
+ if (featuresToDeactivate != FEATURE_NONE) {
+ uint32_t remainingFeatures = Global::get().pluginManager->deactivateFeaturesFor(plugin->getID(), featuresToDeactivate);
+
+ if (remainingFeatures != FEATURE_NONE) {
+ Global::get().l->log(Log::Warning, tr("Unable to deactivate all requested features for plugin \"%1\"").arg(plugin->getName()));
+ }
+ }
+ } else {
+ // loading failed
+ enable = false;
+ Global::get().l->log(Log::Warning, tr("Unable to load plugin \"%1\"").arg(plugin->getName()));
+ }
+ } else {
+ Global::get().pluginManager->unloadPlugin(plugin->getID());
+ }
+
+ QString pluginKey = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ s.qhPluginSettings.insert(pluginKey, { plugin->getFilePath(), enable, positionalDataEnabled, keyboardMonitoringEnabled });
+ }
+ }
+}
+
+const_plugin_ptr_t PluginConfig::pluginForItem(QTreeWidgetItem *i) const {
+ if (i) {
+ return Global::get().pluginManager->getPlugin(i->data(0, Qt::UserRole).toUInt());
+ }
+
+ return nullptr;
+}
+
+void PluginConfig::on_qpbConfig_clicked() {
+ const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem());
+
+ if (plugin) {
+ if (!plugin->showConfigDialog(this)) {
+ // if the plugin doesn't support showing such a dialog, we'll show a default one
+ QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ }
+}
+
+void PluginConfig::on_qpbAbout_clicked() {
+ const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem());
+
+ if (plugin) {
+ if (!plugin->showAboutDialog(this)) {
+ // if the plugin doesn't support showing such a dialog, we'll show a default one
+ QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ }
+}
+
+void PluginConfig::on_qpbReload_clicked() {
+ Global::get().pluginManager->rescanPlugins();
+ refillPluginList();
+}
+
+void PluginConfig::on_qpbUnload_clicked() {
+ QTreeWidgetItem *currentItem = qtwPlugins->currentItem();
+ if (!currentItem) {
+ return;
+ }
+
+ const_plugin_ptr_t plugin = pluginForItem(currentItem);
+ if (!plugin) {
+ return;
+ }
+
+ if (Global::get().pluginManager->clearPlugin(plugin->getID())) {
+ // Plugin was successfully cleared
+ currentItem = qtwPlugins->takeTopLevelItem(qtwPlugins->indexOfTopLevelItem(currentItem));
+
+ delete currentItem;
+ } else {
+ qWarning("PluginConfig.cpp: Failed to delete unloaded plugin entry");
+ }
+}
+
+void PluginConfig::refillPluginList() {
+ qtwPlugins->clear();
+
+ // get plugins already sorted according to their name
+ const QVector<const_plugin_ptr_t > plugins = Global::get().pluginManager->getPlugins(true);
+
+ foreach(const_plugin_ptr_t currentPlugin, plugins) {
+ QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins);
+ i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ i->setCheckState(1, currentPlugin->isLoaded() ? Qt::Checked : Qt::Unchecked);
+
+ if (currentPlugin->getFeatures() & FEATURE_POSITIONAL) {
+ i->setCheckState(2, currentPlugin->isPositionalDataEnabled() ? Qt::Checked : Qt::Unchecked);
+ i->setToolTip(2, tr("Whether the positional audio feature of this plugin should be enabled"));
+ } else {
+ i->setToolTip(2, tr("This plugin does not provide support for positional audio"));
+ }
+
+ i->setCheckState(3, currentPlugin->isKeyboardMonitoringAllowed() ? Qt::Checked : Qt::Unchecked);
+ i->setToolTip(3, tr("Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus"));
+
+ i->setText(0, currentPlugin->getName());
+ i->setToolTip(0, currentPlugin->getDescription().toHtmlEscaped());
+ i->setToolTip(1, tr("Whether this plugin should be enabled"));
+ i->setData(0, Qt::UserRole, currentPlugin->getID());
+ }
+
+ qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0));
+ on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), NULL);
+}
+
+void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) {
+ const_plugin_ptr_t plugin = pluginForItem(current);
+
+ if (plugin) {
+ qpbAbout->setEnabled(plugin->providesAboutDialog());
+
+ qpbConfig->setEnabled(plugin->providesConfigDialog());
+
+ qpbUnload->setEnabled(true);
+ } else {
+ qpbAbout->setEnabled(false);
+ qpbConfig->setEnabled(false);
+ qpbUnload->setEnabled(false);
+ }
+}
diff --git a/src/mumble/PluginConfig.h b/src/mumble/PluginConfig.h
new file mode 100644
index 000000000..cab9ce7b3
--- /dev/null
+++ b/src/mumble/PluginConfig.h
@@ -0,0 +1,66 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINS_H_
+#define MUMBLE_MUMBLE_PLUGINS_H_
+
+#include "ConfigDialog.h"
+#include "ui_PluginConfig.h"
+#include "Plugin.h"
+
+#include <QtCore/QObject>
+#include <QtCore/QMutex>
+#include <QtCore/QReadWriteLock>
+
+struct PluginInfo;
+
+class PluginConfig : public ConfigWidget, public Ui::PluginConfig {
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(PluginConfig)
+ protected:
+ /// Clears and (re-) populates the plugin list in the UI with the currently available plugins
+ void refillPluginList();
+ /// @param item The QTreeWidgetItem to retrieve the plugin for
+ /// @returns The plugin corresponding to the provided item
+ const_plugin_ptr_t pluginForItem(QTreeWidgetItem *item) const;
+ public:
+ /// The unique name of this ConfigWidget
+ static const QString name;
+ /// Constructor
+ ///
+ /// @param st The settings object to work on
+ PluginConfig(Settings &st);
+ /// @returns The title of this widget
+ virtual QString title() const Q_DECL_OVERRIDE;
+ /// @returns The name of this ConfigWidget
+ const QString &getName() const Q_DECL_OVERRIDE;
+ /// @returns The icon for this widget
+ virtual QIcon icon() const Q_DECL_OVERRIDE;
+ public slots:
+ /// Saves the current configuration to the respective settings object
+ void save() const Q_DECL_OVERRIDE;
+ /// Loads the transmit-position from the provided settings object
+ ///
+ /// @param The setting sobject to read from
+ void load(const Settings &r) Q_DECL_OVERRIDE;
+ /// Slot triggered when the install-button in the UI has been clicked
+ void on_qpbInstallPlugin_clicked();
+ /// Slot triggered when the config-button in the UI has been clicked
+ void on_qpbConfig_clicked();
+ /// Slot triggered when the about-button in the UI has been clicked
+ void on_qpbAbout_clicked();
+ /// Slot triggered when the reload-button in the UI has been clicked
+ void on_qpbReload_clicked();
+ /// Slot triggered when the unload-button in the UI has been clicked
+ void on_qpbUnload_clicked();
+ /// Slot triggered when the selection in the plugin list hast changed
+ ///
+ /// @param current The currently selected item
+ /// @param old The previously selected item (if applicable - otherwise NULL/nullptr)
+ void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *old);
+};
+
+#endif
diff --git a/src/mumble/Plugins.ui b/src/mumble/PluginConfig.ui
index b634d3f0b..fff242cac 100644
--- a/src/mumble/Plugins.ui
+++ b/src/mumble/PluginConfig.ui
@@ -6,14 +6,14 @@
<rect>
<x>0</x>
<y>0</y>
- <width>321</width>
- <height>235</height>
+ <width>570</width>
+ <height>289</height>
</rect>
</property>
<property name="windowTitle">
<string>Plugins</string>
</property>
- <layout class="QVBoxLayout">
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="qgbOptions">
<property name="title">
@@ -53,9 +53,6 @@
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
<column>
<property name="text">
<string>Name</string>
@@ -63,7 +60,17 @@
</column>
<column>
<property name="text">
- <string>Enabled</string>
+ <string>Enable</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>PA</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>KeyEvents</string>
</property>
</column>
</widget>
@@ -84,6 +91,16 @@
</widget>
</item>
<item>
+ <widget class="QPushButton" name="qpbInstallPlugin">
+ <property name="toolTip">
+ <string>Install a plugin from a local file</string>
+ </property>
+ <property name="text">
+ <string>Install plugin...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -122,6 +139,16 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QPushButton" name="qpbUnload">
+ <property name="toolTip">
+ <string>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</string>
+ </property>
+ <property name="text">
+ <string>Unload</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
diff --git a/src/mumble/PluginInstaller.cpp b/src/mumble/PluginInstaller.cpp
new file mode 100644
index 000000000..41494d64e
--- /dev/null
+++ b/src/mumble/PluginInstaller.cpp
@@ -0,0 +1,200 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtCore/QString>
+#include <QtCore/QException>
+#include <QtCore/QObject>
+#include <QtCore/QStringList>
+#include <QtCore/QDir>
+
+#include <QtGui/QIcon>
+
+#include <exception>
+#include <string>
+#include <fstream>
+
+#include <Poco/Zip/ZipArchive.h>
+#include <Poco/Zip/ZipStream.h>
+#include <Poco/StreamCopier.h>
+#include <Poco/Exception.h>
+
+PluginInstallException::PluginInstallException(const QString& msg)
+ : m_msg(msg) {
+}
+
+QString PluginInstallException::getMessage() const {
+ return m_msg;
+}
+
+const QString PluginInstaller::pluginFileExtension = QLatin1String("mumble_plugin");
+
+bool PluginInstaller::canBePluginFile(const QFileInfo& fileInfo) noexcept {
+ if (!fileInfo.isFile()) {
+ // A plugin file has to be a file (obviously)
+ return false;
+ }
+
+ if (fileInfo.suffix().compare(PluginInstaller::pluginFileExtension, Qt::CaseInsensitive) == 0
+ || fileInfo.suffix().compare(QLatin1String("zip"), Qt::CaseInsensitive) == 0) {
+ // A plugin file has either the extension given in PluginInstaller::pluginFileExtension or zip
+ return true;
+ }
+
+ // We might also accept a shared library directly
+ return QLibrary::isLibrary(fileInfo.fileName());
+}
+
+PluginInstaller::PluginInstaller(const QFileInfo& fileInfo, QWidget *p)
+ : QDialog(p),
+ m_pluginArchive(fileInfo),
+ m_plugin(nullptr),
+ m_pluginSource(),
+ m_pluginDestination(),
+ m_copyPlugin(false) {
+ setupUi(this);
+
+ setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
+
+ QObject::connect(qpbYes, &QPushButton::clicked, this, &PluginInstaller::on_qpbYesClicked);
+ QObject::connect(qpbNo, &QPushButton::clicked, this, &PluginInstaller::on_qpbNoClicked);
+
+ init();
+}
+
+PluginInstaller::~PluginInstaller() {
+ if (m_plugin) {
+ delete m_plugin;
+ }
+}
+
+void PluginInstaller::init() {
+ if (!PluginInstaller::canBePluginFile(m_pluginArchive)) {
+ throw PluginInstallException(tr("The file \"%1\" is not a valid plugin file!").arg(m_pluginArchive.fileName()));
+ }
+
+ if (QLibrary::isLibrary(m_pluginArchive.fileName())) {
+ // For a library the fileInfo provided is already the actual plugin library
+ m_pluginSource = m_pluginArchive;
+
+ m_copyPlugin = true;
+ } else {
+ // We have been provided with a zip-file
+ try {
+ std::ifstream zipInput(m_pluginArchive.filePath().toStdString());
+ Poco::Zip::ZipArchive archive(zipInput);
+
+ // Iterate over all files in the archive to see which ones could be the correct plugin library
+ QString pluginName;
+ auto it = archive.fileInfoBegin();
+ while (it != archive.fileInfoEnd()) {
+ QString currentFileName = QString::fromStdString(it->first);
+ if (QLibrary::isLibrary(currentFileName)) {
+ if (!pluginName.isEmpty()) {
+ // There seem to be multiple plugins in here. That's not allowed
+ throw PluginInstallException(tr("Found more than one plugin library for the current OS in \"%1\" (\"%2\" and \"%3\")!").arg(
+ m_pluginArchive.fileName()).arg(pluginName).arg(currentFileName));
+ }
+
+ pluginName = currentFileName;
+ }
+
+ it++;
+ }
+
+ if (pluginName.isEmpty()) {
+ throw PluginInstallException(tr("Unable to find a plugin for the current OS in \"%1\"").arg(m_pluginArchive.fileName()));
+ }
+
+ // Unpack the plugin library into the tmp dir
+ // We don't have to create the directory structure as we're only interested in the library itself
+ QString tmpPluginPath = QDir::temp().filePath(QFileInfo(pluginName).fileName());
+ auto pluginIt = archive.findHeader(pluginName.toStdString());
+ zipInput.clear();
+ Poco::Zip::ZipInputStream zipin(zipInput, pluginIt->second);
+ std::ofstream out(tmpPluginPath.toStdString());
+ Poco::StreamCopier::copyStream(zipin, out);
+
+ m_pluginSource = QFileInfo(tmpPluginPath);
+ } catch(const Poco::Exception &e) {
+ // Something didn't work out during the Zip processing
+ throw PluginInstallException(QString::fromStdString(std::string("Failed to process zip archive: ") + e.message()));
+ }
+ }
+
+ QString pluginFileName = m_pluginSource.fileName();
+
+ // Try to load the plugin up to see if it is actually valid
+ try {
+ m_plugin = Plugin::createNew<Plugin>(m_pluginSource.absoluteFilePath());
+ } catch(const PluginError&) {
+ throw PluginInstallException(tr("Unable to load plugin \"%1\" - check the plugin interface!").arg(pluginFileName));
+ }
+
+ m_pluginDestination = QFileInfo(QString::fromLatin1("%1/%2").arg(getInstallDir()).arg(pluginFileName));
+
+
+ // Now that we located the plugin, it is time to fill in its details in the UI
+ qlName->setText(m_plugin->getName());
+
+ mumble_version_t pluginVersion = m_plugin->getVersion();
+ mumble_version_t usedAPIVersion = m_plugin->getAPIVersion();
+ qlVersion->setText(QString::fromLatin1("%1 (API %2)").arg(pluginVersion == VERSION_UNKNOWN ?
+ "Unknown" : static_cast<QString>(pluginVersion)).arg(
+ usedAPIVersion == VERSION_UNKNOWN ? "Unknown" : static_cast<QString>(usedAPIVersion)));
+
+ qlAuthor->setText(m_plugin->getAuthor());
+
+ qlDescription->setText(m_plugin->getDescription());
+}
+
+void PluginInstaller::install() const {
+ if (!m_plugin) {
+ // This function shouldn't even be called, if the plugin object has not been created...
+ throw PluginInstallException(QLatin1String("[INTERNAL ERROR]: Trying to install an invalid plugin"));
+ }
+
+ if (m_pluginSource == m_pluginDestination) {
+ // Apparently the plugin is already installed
+ return;
+ }
+
+ if (m_pluginDestination.exists()) {
+ // Delete old version first
+ if (!QFile(m_pluginDestination.absoluteFilePath()).remove()) {
+ throw PluginInstallException(tr("Unable to delete old plugin at \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
+ }
+ }
+
+ if (m_copyPlugin) {
+ if (!QFile(m_pluginSource.absoluteFilePath()).copy(m_pluginDestination.absoluteFilePath())) {
+ throw PluginInstallException(tr("Unable to copy plugin library from \"%1\" to \"%2\"").arg(m_pluginSource.absoluteFilePath()).arg(
+ m_pluginDestination.absoluteFilePath()));
+ }
+ } else {
+ // Move the plugin into the respective dir
+ if (!QFile(m_pluginSource.absoluteFilePath()).rename(m_pluginDestination.absoluteFilePath())) {
+ throw PluginInstallException(tr("Unable to move plugin library to \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
+ }
+ }
+}
+
+QString PluginInstaller::getInstallDir() {
+ // Get the path to the plugin-dir in "user-land" (aka: the user definitely has write access to this
+ // location).
+ return Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins");
+}
+
+void PluginInstaller::on_qpbYesClicked() {
+ install();
+
+ accept();
+}
+
+void PluginInstaller::on_qpbNoClicked() {
+ close();
+}
diff --git a/src/mumble/PluginInstaller.h b/src/mumble/PluginInstaller.h
new file mode 100644
index 000000000..d8df3c303
--- /dev/null
+++ b/src/mumble/PluginInstaller.h
@@ -0,0 +1,84 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGININSTALLER_H_
+#define MUMBLE_MUMBLE_PLUGININSTALLER_H_
+
+
+#include <QtCore/QFileInfo>
+#include <QtCore/QException>
+
+#include "Plugin.h"
+
+#include "ui_PluginInstaller.h"
+
+/// An exception thrown by the PluginInstaller
+class PluginInstallException : public QException {
+ protected:
+ /// The exception's message
+ QString m_msg;
+ public:
+ /// @param msg The message stating why this exception has been thrown
+ PluginInstallException(const QString& msg);
+
+ /// @returns This exception's message
+ QString getMessage() const;
+};
+
+/// The PluginInstaller can be used to install plugins into Mumble. It verifies that the respective
+/// plugin is functional and will automatiacally copy/move the plugin library to the respective
+/// directory on the FileSystem.
+class PluginInstaller : public QDialog, public Ui::PluginInstaller {
+ private:
+ Q_OBJECT;
+ Q_DISABLE_COPY(PluginInstaller);
+ protected:
+ /// The file the installer has been invoked on
+ QFileInfo m_pluginArchive;
+ /// A pointer to the plugin instance created from the plugin library that shall be installed
+ Plugin *m_plugin;
+ /// The actual plugin library file
+ QFileInfo m_pluginSource;
+ /// The destinaton file to which the plugin library shall be copied
+ QFileInfo m_pluginDestination;
+ /// A flag indicating that the plugin library shall be copied instead of moved in order
+ /// to install it.
+ bool m_copyPlugin;
+
+ /// Initializes this installer by processing the provided plugin source and filling all
+ /// internal fields. This function is called from the constructor.
+ ///
+ /// @throws PluginInstallException If something isn't right or goes wrong
+ void init();
+ public:
+ /// The "special" file-extension associated with Mumble plugins
+ static const QString pluginFileExtension;
+
+ /// A helper function checking whether the provided file could be a plugin source
+ ///
+ /// @param fileInfo The file to check
+ /// @returns Whether the provided file could (!) be a plugin source
+ static bool canBePluginFile(const QFileInfo& fileInfo) noexcept;
+
+ /// @param fileInfo The plugin source to process
+ ///
+ /// @throws PluginInstallException If something isn't right or goes wrong
+ PluginInstaller(const QFileInfo& fileInfo, QWidget *p = nullptr);
+ /// Destructor
+ ~PluginInstaller();
+
+ /// Performs the actual installation (moving/copying of the library) of the plugin
+ void install() const;
+
+ static QString getInstallDir();
+
+ public slots:
+ /// Slot called when the user clicks the yes button
+ void on_qpbYesClicked();
+ /// Slot called when the user clicks the no button
+ void on_qpbNoClicked();
+};
+
+#endif // MUMBLE_MUMBLE_PLUGININSTALLER_H_
diff --git a/src/mumble/PluginInstaller.ui b/src/mumble/PluginInstaller.ui
new file mode 100644
index 000000000..39f575dea
--- /dev/null
+++ b/src/mumble/PluginInstaller.ui
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginInstaller</class>
+ <widget class="QDialog" name="PluginInstaller">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>360</width>
+ <height>332</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginInstaller</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlPrompt">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="text">
+ <string>You are about to install the plugin listed below. Do you wish to proceed?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwPluginInfo">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>348</width>
+ <height>208</height>
+ </rect>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="verticalSpacing">
+ <number>12</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlName_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlName">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="qlVersion_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="qlVersion">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="qlAuthor_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="qlAuthor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="qlDescription_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="qlDescription">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="qpbNo">
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;No</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="qpbYes">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;Yes</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/mumble/PluginManager.cpp b/src/mumble/PluginManager.cpp
new file mode 100644
index 000000000..817eb69be
--- /dev/null
+++ b/src/mumble/PluginManager.cpp
@@ -0,0 +1,933 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include <limits>
+
+#include "PluginManager.h"
+#include "LegacyPlugin.h"
+#include <QReadLocker>
+#include <QWriteLocker>
+#include <QReadLocker>
+#include <QDir>
+#include <QFileInfoList>
+#include <QFileInfo>
+#include <QVector>
+#include <QByteArray>
+#include <QChar>
+#include <QMutexLocker>
+#include <QHashIterator>
+#include <QKeyEvent>
+#include <QTimer>
+
+#include "ManualPlugin.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "ProcessResolver.h"
+#include "ServerHandler.h"
+#include "PluginUpdater.h"
+#include "API.h"
+#include "Global.h"
+
+#include <memory>
+
+#ifdef Q_OS_WIN
+ #include <tlhelp32.h>
+ #include <string>
+#endif
+
+#ifdef Q_OS_LINUX
+ #include <QtCore/QStringList>
+#endif
+
+PluginManager::PluginManager(QSet<QString> *additionalSearchPaths, QObject *p)
+ : QObject(p),
+ m_pluginCollectionLock(QReadWriteLock::NonRecursive),
+ m_pluginHashMap(),
+ m_positionalData(),
+ m_positionalDataCheckTimer(),
+ m_sentDataMutex(),
+ m_sentData(),
+ m_activePosDataPluginLock(QReadWriteLock::NonRecursive),
+ m_activePositionalDataPlugin(),
+ m_updater() {
+
+ // Setup search-paths
+ if (additionalSearchPaths) {
+ for (const auto &currentPath : *additionalSearchPaths) {
+ m_pluginSearchPaths.insert(currentPath);
+ }
+ }
+
+#ifdef Q_OS_MAC
+ // Path to plugins inside AppBundle
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()));
+#endif
+
+#ifdef MUMBLE_PLUGIN_PATH
+ // Path to where plugins are/will be installed on the system
+ m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH)));
+#endif
+
+ // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble
+ // is run after compilation without having installed it anywhere special
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()));
+
+ // Path to where the plugin installer will write plugins
+ m_pluginSearchPaths.insert(PluginInstaller::getInstallDir());
+
+#ifdef Q_OS_WIN
+ // According to MS KB Q131065, we need this to OpenProcess()
+
+ m_hToken = nullptr;
+
+ if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) {
+ if (GetLastError() == ERROR_NO_TOKEN) {
+ ImpersonateSelf(SecurityImpersonation);
+ OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken);
+ }
+ }
+
+ TOKEN_PRIVILEGES tp;
+ LUID luid;
+ m_cbPrevious=sizeof(TOKEN_PRIVILEGES);
+
+ LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
+
+ tp.PrivilegeCount = 1;
+ tp.Privileges[0].Luid = luid;
+ tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
+
+ AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious);
+#endif
+
+ // Synchronize the positional data in a regular interval
+ // By making this the parent of the created timer, we don't have to delete it explicitly
+ QTimer *serverSyncTimer = new QTimer(this);
+ QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData);
+ serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL);
+
+ // Install this manager as a global eventFilter in order to get notified about all keypresses
+ if (QCoreApplication::instance()) {
+ QCoreApplication::instance()->installEventFilter(this);
+ }
+
+ // Set up the timer for regularly checking for available positional data plugins
+ m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL);
+ m_positionalDataCheckTimer.start();
+ QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin);
+
+ QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable);
+ QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent);
+}
+
+PluginManager::~PluginManager() {
+ clearPlugins();
+
+#ifdef Q_OS_WIN
+ AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL);
+ CloseHandle(m_hToken);
+#endif
+}
+
+/// Emits a log about a plugin with the given name having lost link (positional audio)
+///
+/// @param pluginName The name of the plugin that lost link
+void reportLostLink(const QString& pluginName) {
+ Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped()));
+}
+
+bool PluginManager::eventFilter(QObject *target, QEvent *event) {
+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
+ static QVector<QKeyEvent*> processedEvents;
+
+ QKeyEvent *kEvent = static_cast<QKeyEvent *>(event);
+
+ // We have to keep track of which events we have processed already as
+ // the same event might be sent to multiple targets and since this is
+ // installed as a global event filter, we get notified about each of
+ // them. However we want to process each event only once.
+ if (!kEvent->isAutoRepeat() && !processedEvents.contains(kEvent)) {
+ // Fire event
+ emit keyEvent(kEvent->key(), kEvent->modifiers(), kEvent->type() == QEvent::KeyPress);
+
+ processedEvents << kEvent;
+
+ if (processedEvents.size() == 1) {
+ // Make sure to clear the list of processed events after each iteration
+ // of the event loop (we don't want to let the vector grow to infinity
+ // over time. Firing the timer only when the size of processedEvents is
+ // exactly 1, we avoid adding multiple timers in a single iteration.
+ QTimer::singleShot(0, []() { processedEvents.clear(); });
+ }
+ }
+
+ }
+
+ // standard event processing
+ return QObject::eventFilter(target, event);
+}
+
+void PluginManager::unloadPlugins() const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.begin();
+
+ while (it != m_pluginHashMap.end()) {
+ unloadPlugin(*it.value());
+ it++;
+ }
+}
+
+void PluginManager::clearPlugins() {
+ // Unload plugins so that they aren't implicitly unloaded once they go out of scope after having been
+ // removed from the pluginHashMap.
+ // This could lead to one of the plugins making an API call in its shutdown function which then would try
+ // to verify the plugin's ID. For that it'll ask this PluginManager for a plugin with that ID. To check
+ // that it will have to aquire a read-lock for the pluginHashMap which is impossible after we aquire the
+ // write-lock in this function leading to a deadlock.
+ unloadPlugins();
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Clear the list itself
+ m_pluginHashMap.clear();
+}
+
+bool PluginManager::selectActivePositionalDataPlugin() {
+ QReadLocker pluginLock(&m_pluginCollectionLock);
+ QWriteLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!Global::get().s.bTransmitPosition) {
+ // According to the settings the position shall not be transmitted meaning that we don't have to select any plugin
+ // for positional data
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+ }
+
+ ProcessResolver procRes(true);
+
+ auto it = m_pluginHashMap.begin();
+
+ // We assume that there is only one (enabled) plugin for the currently played game so we don't have to remember
+ // which plugin was active last
+ while (it != m_pluginHashMap.end()) {
+ plugin_ptr_t currentPlugin = it.value();
+
+ if (currentPlugin->isPositionalDataEnabled() && currentPlugin->isLoaded()) {
+ switch(currentPlugin->initPositionalData(procRes.getProcessNames().data(),
+ procRes.getProcessPIDs().data(), procRes.amountOfProcesses())) {
+ case PDEC_OK:
+ // the plugin is ready to provide positional data
+ m_activePositionalDataPlugin = currentPlugin;
+
+ Global::get().l->log(Log::Information, tr("%1 linked").arg(currentPlugin->getName().toHtmlEscaped()));
+
+ return true;
+
+ case PDEC_ERROR_PERM:
+ // the plugin encountered a permanent error -> disable it
+ Global::get().l->log(Log::Warning, tr(
+ "Plugin \"%1\" encountered a permanent error in positional data gathering").arg(currentPlugin->getName()));
+
+ currentPlugin->enablePositionalData(false);
+ break;
+
+ case PDEC_ERROR_TEMP:
+ //The plugin encountered a temporary error -> skip it for now (that is: do nothing)
+ break;
+ }
+ }
+
+ it++;
+ }
+
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+}
+
+#define LOG_FOUND(plugin, path, legacyStr) qDebug("Found %splugin '%s' at \"%s\"", legacyStr, qUtf8Printable(plugin->getName()), qUtf8Printable(path));\
+ qDebug() << "Its description:" << qUtf8Printable(plugin->getDescription())
+#define LOG_FOUND_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "")
+#define LOG_FOUND_LEGACY_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "legacy ")
+#define LOG_FOUND_BUILTIN(plugin) LOG_FOUND(plugin, QString::fromLatin1("<builtin>"), "built-in ")
+void PluginManager::rescanPlugins() {
+ clearPlugins();
+
+ {
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // iterate over all files in the respective directories and try to construct a plugin from them
+ for (const auto &currentPath : m_pluginSearchPaths) {
+ QFileInfoList currentList = QDir(currentPath).entryInfoList();
+
+ for (int k=0; k<currentList.size(); k++) {
+ QFileInfo currentInfo = currentList[k];
+
+ if (!QLibrary::isLibrary(currentInfo.absoluteFilePath())) {
+ // consider only files that actually could be libraries
+ continue;
+ }
+
+ try {
+ plugin_ptr_t p(Plugin::createNew<Plugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_PLUGIN(p, currentInfo.absoluteFilePath());
+#endif
+
+ // if this code block is reached, the plugin was instantiated successfully so we can add it to the map
+ m_pluginHashMap.insert(p->getID(), p);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+ // If an exception is thrown, this library does not represent a proper plugin
+ // Check if it might be a legacy plugin instead
+ try {
+ legacy_plugin_ptr_t lp(Plugin::createNew<LegacyPlugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_LEGACY_PLUGIN(lp, currentInfo.absoluteFilePath());
+#endif
+ m_pluginHashMap.insert(lp->getID(), lp);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning,
+ tr("Non-plugin found in plugin directory: \"%1\"").arg(currentInfo.absoluteFilePath()));
+ }
+ }
+ }
+ }
+
+ // handle built-in plugins
+#ifdef USE_MANUAL_PLUGIN
+ try {
+ std::shared_ptr<ManualPlugin> mp(Plugin::createNew<ManualPlugin>());
+
+ m_pluginHashMap.insert(mp->getID(), mp);
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_BUILTIN(mp);
+#endif
+ } catch(const PluginError& e) {
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning, tr("Failed at loading manual plugin: %1").arg(QString::fromUtf8(e.what())));
+ }
+#endif
+ }
+
+ QReadLocker readLock(&m_pluginCollectionLock);
+
+ // load plugins based on settings
+ // iterate over all plugins that have saved settings
+ auto it = Global::get().s.qhPluginSettings.constBegin();
+ while (it != Global::get().s.qhPluginSettings.constEnd()) {
+ // for this we need a way to get a plugin based on the filepath
+ const QString pluginKey = it.key();
+ const PluginSetting setting = it.value();
+
+ // iterate over all loaded plugins to see if the current setting is applicable
+ auto pluginIt = m_pluginHashMap.begin();
+ while (pluginIt != m_pluginHashMap.end()) {
+ plugin_ptr_t plugin = pluginIt.value();
+ QString pluginHash = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ if (pluginKey == pluginHash) {
+ if (setting.enabled) {
+ loadPlugin(plugin->getID());
+
+ const uint32_t features = plugin->getFeatures();
+
+ if (!setting.positionalDataEnabled && (features & FEATURE_POSITIONAL)) {
+ // try to deactivate the feature if the setting says so
+ plugin->deactivateFeatures(FEATURE_POSITIONAL);
+ }
+ }
+
+ // positional data is a special feature that has to be enabled/disabled in the Plugin wrapper class
+ // additionally to telling the plugin library that the feature shall be deactivated
+ plugin->enablePositionalData(setting.positionalDataEnabled);
+
+ plugin->allowKeyboardMonitoring(setting.allowKeyboardMonitoring);
+
+ break;
+ }
+
+ pluginIt++;
+ }
+
+ it++;
+ }
+}
+
+const_plugin_ptr_t PluginManager::getPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.value(pluginID);
+}
+
+void PluginManager::checkForPluginUpdates() {
+ m_updater.checkForUpdates();
+}
+
+bool PluginManager::fetchPositionalData() {
+ if (Global::get().bPosTest) {
+ // This is for testing-purposes only so the "fetched" position doesn't have any real meaning
+ m_positionalData.reset();
+
+ m_positionalData.m_playerDir.z = 1.0f;
+ m_positionalData.m_playerAxis.y = 1.0f;
+ m_positionalData.m_cameraDir.z = 1.0f;
+ m_positionalData.m_cameraAxis.y = 1.0f;
+
+ return true;
+ }
+
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!m_activePositionalDataPlugin) {
+ // It appears as if there is currently no plugin capable of delivering positional audio
+ // Set positional data to zero-values
+ m_positionalData.reset();
+
+ return false;
+ }
+
+ QWriteLocker posDataLock(&m_positionalData.m_lock);
+
+ bool retStatus = m_activePositionalDataPlugin->fetchPositionalData(m_positionalData.m_playerPos, m_positionalData.m_playerDir,
+ m_positionalData.m_playerAxis, m_positionalData.m_cameraPos, m_positionalData.m_cameraDir, m_positionalData.m_cameraAxis,
+ m_positionalData.m_context, m_positionalData.m_identity);
+
+ // Add the plugin's name to the context as well to prevent name-clashes between plugins
+ if (!m_positionalData.m_context.isEmpty()) {
+ m_positionalData.m_context = m_activePositionalDataPlugin->getName() + QChar::Null + m_positionalData.m_context;
+ }
+
+ if (!retStatus) {
+ // Shut the currently active plugin down and set a new one (if available)
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock
+ activePluginLock.unlock();
+
+ selectActivePositionalDataPlugin();
+ } else {
+ // If the return-status doesn't indicate an error, we can assume that positional data is available
+ // The remaining problematic case is, if the player is exactly at position (0,0,0) as this is used as an indicator for the
+ // absence of positional data in the mix() function in AudioOutput.cpp
+ // Thus we have to make sure that this position is never set if positional data is actually available.
+ // We solve this problem by shifting the player a minimal amount on the z-axis
+ if (m_positionalData.m_playerPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_playerPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ if (m_positionalData.m_cameraPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_cameraPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ }
+
+ return retStatus;
+}
+
+void PluginManager::unlinkPositionalData() {
+ QWriteLocker lock(&m_activePosDataPluginLock);
+
+ if (m_activePositionalDataPlugin) {
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // Set the pointer to nullptr
+ m_activePositionalDataPlugin = nullptr;
+ }
+}
+
+bool PluginManager::isPositionalDataAvailable() const {
+ QReadLocker lock(&m_activePosDataPluginLock);
+
+ return m_activePositionalDataPlugin != nullptr;
+}
+
+const PositionalData& PluginManager::getPositionalData() const {
+ return m_positionalData;
+}
+
+void PluginManager::enablePositionalDataFor(plugin_id_t pluginID, bool enable) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ plugin->enablePositionalData(enable);
+ }
+}
+
+const QVector<const_plugin_ptr_t > PluginManager::getPlugins(bool sorted) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ QVector<const_plugin_ptr_t> pluginList;
+
+ auto it = m_pluginHashMap.constBegin();
+ if (sorted) {
+ QList<plugin_id_t> ids = m_pluginHashMap.keys();
+
+ // sort keys so that the corresponding Plugins are in alphabetical order based on their name
+ std::sort(ids.begin(), ids.end(), [this](plugin_id_t first, plugin_id_t second) {
+ return QString::compare(m_pluginHashMap.value(first)->getName(), m_pluginHashMap.value(second)->getName(),
+ Qt::CaseInsensitive) <= 0;
+ });
+
+ foreach(plugin_id_t currentID, ids) {
+ pluginList.append(m_pluginHashMap.value(currentID));
+ }
+ } else {
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginList.append(it.value());
+
+ it++;
+ }
+ }
+
+ return pluginList;
+}
+
+bool PluginManager::loadPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ if (plugin->isLoaded()) {
+ // Don't attempt to load a plugin if it already is loaded.
+ // This can happen if the user clicks the apply button in the settings
+ // before hitting ok.
+ return true;
+ }
+
+ return plugin->init() == STATUS_OK;
+ }
+
+ return false;
+}
+
+void PluginManager::unloadPlugin(plugin_id_t pluginID) const {
+ plugin_ptr_t plugin;
+ {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin = m_pluginHashMap.value(pluginID);
+ }
+
+ if (plugin) {
+ unloadPlugin(*plugin);
+ }
+}
+
+void PluginManager::unloadPlugin(Plugin &plugin) const {
+ if (plugin.isLoaded()) {
+ // Only shut down loaded plugins
+ plugin.shutdown();
+ }
+}
+
+bool PluginManager::clearPlugin(plugin_id_t pluginID) {
+ // We have to unload the plugin before we take the write lock. The reasoning being that if
+ // the plugin makes an API call in its shutdown callback, that'll lead to this manager being
+ // asked for whether a plugin with such an ID exists. The function performing this check will
+ // take a read lock which is not possible if we hold a write lock here already (deadlock).
+ unloadPlugin(pluginID);
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Remove the plugin from the list of known plugins
+ plugin_ptr_t plugin = m_pluginHashMap.take(pluginID);
+
+ return plugin != nullptr;
+}
+
+uint32_t PluginManager::deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->deactivateFeatures(features);
+ }
+
+ return FEATURE_NONE;
+}
+
+void PluginManager::allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->allowKeyboardMonitoring(allow);
+ }
+}
+
+bool PluginManager::pluginExists(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.contains(pluginID);
+}
+
+void PluginManager::foreachPlugin(std::function<void(Plugin&)> pluginProcessor) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.constBegin();
+
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginProcessor(*it.value());
+
+ it++;
+ }
+}
+
+void PluginManager::on_serverConnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Connected to a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerConnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_serverDisconnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Disconnected from a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerDisconnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "entered channel" << newChannel->qsName << "- ID:" << newChannel->iId;
+#endif
+
+ if (!Global::get().sh) {
+ // if there is no server-handler, there is no (real) channel to enter
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, newChannel, prevChannel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelEntered(connectionID, user->uiSession, prevChannel ? prevChannel->iId : -1, newChannel->iId);
+ }
+ });
+}
+
+void PluginManager::on_channelExited(const Channel *channel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "left channel" << channel->qsName << "- ID:" << channel->iId;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, channel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelExited(connectionID, user->uiSession, channel->iId);
+ }
+ });
+}
+
+QString getTalkingStateStr(Settings::TalkState ts) {
+ switch(ts) {
+ case Settings::TalkState::Passive:
+ return QString::fromLatin1("Passive");
+ case Settings::TalkState::Talking:
+ return QString::fromLatin1("Talking");
+ case Settings::TalkState::Whispering:
+ return QString::fromLatin1("Whispering");
+ case Settings::TalkState::Shouting:
+ return QString::fromLatin1("Shouting");
+ case Settings::TalkState::MutedTalking:
+ return QString::fromLatin1("MutedTalking");
+ }
+
+ return QString::fromLatin1("Unknown");
+}
+
+void PluginManager::on_userTalkingStateChanged() const {
+ const ClientUser *user = qobject_cast<ClientUser*>(QObject::sender());
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ if (user) {
+ qDebug() << "PluginManager: User" << user->qsName << "changed talking state to" << getTalkingStateStr(user->tsState);
+ } else {
+ qCritical() << "PluginManager: Unable to identify ClientUser";
+ }
+#endif
+
+ if (user) {
+ // Convert Mumble's talking state to the TalkingState used in the API
+ mumble_talking_state_t ts = INVALID;
+
+ switch(user->tsState) {
+ case Settings::TalkState::Passive:
+ ts = PASSIVE;
+ break;
+ case Settings::TalkState::Talking:
+ ts = TALKING;
+ break;
+ case Settings::TalkState::Whispering:
+ ts = WHISPERING;
+ break;
+ case Settings::TalkState::Shouting:
+ ts = SHOUTING;
+ break;
+ case Settings::TalkState::MutedTalking:
+ ts = TALKING_MUTED;
+ break;
+ }
+
+ if (ts == INVALID) {
+ qWarning("PluginManager.cpp: Invalid talking state encountered");
+ // An error occured
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, ts, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserTalkingStateChanged(connectionID, user->uiSession, ts);
+ }
+ });
+ }
+}
+
+void PluginManager::on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioInput with" << channelCount << "channels and" << sampleCount << "samples per channel. IsSpeech:" << isSpeech;
+#endif
+
+ foreachPlugin([inputPCM, sampleCount, channelCount, sampleRate, isSpeech](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ }
+ });
+}
+
+void PluginManager::on_audioSourceFetched(float* outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser* user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioSource with" << channelCount << "channels and" << sampleCount << "samples per channel fetched. IsSpeech:" << isSpeech;
+ if (user != nullptr) {
+ qDebug() << "Sender-ID:" << user->uiSession;
+ }
+#endif
+
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user ? user->uiSession : -1);
+ }
+ });
+}
+
+void PluginManager::on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioOutput with" << channelCount << "channels and" << sampleCount << "samples per channel";
+#endif
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, modifiedAudio](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ if(plugin.onAudioOutputAboutToPlay(outputPCM, sampleCount, sampleRate, channelCount)) {
+ *modifiedAudio = true;
+ }
+ }
+ });
+}
+
+void PluginManager::on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Data with ID" << dataID << "and length" << dataLength << "received. Sender-ID:" << sender->uiSession;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([sender, data, dataLength, dataID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onReceiveData(connectionID, sender->uiSession, data, dataLength, dataID);
+ }
+ });
+}
+
+void PluginManager::on_serverSynchronized() const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Server synchronized";
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerSynchronized(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_userAdded(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserAdded(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_userRemoved(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserRemoved(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_channelAdded(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelAdded(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRemoved(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRemoved(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRenamed(int channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Renamed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRenamed(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Key event detected: keyCode =" << key << "modifiers:"
+ << modifiers << "isPress =" << isPress;
+#else
+ Q_UNUSED(modifiers);
+#endif
+
+ // Convert from Qt encoding to our own encoding
+ mumble_keycode_t keyCode = API::qtKeyCodeToAPIKeyCode(key);
+
+ foreachPlugin([keyCode, isPress](Plugin &plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onKeyEvent(keyCode, isPress);
+ }
+ });
+}
+
+void PluginManager::on_syncPositionalData() {
+ // fetch positional data
+ if (fetchPositionalData()) {
+ // Sync the gathered data (context + identity) with the server
+ if (!Global::get().uiSession) {
+ // For some reason the local session ID is not set -> clear all data sent to the server in order to gurantee
+ // a re-send once the session is restored and there is data available
+ QMutexLocker mLock(&m_sentDataMutex);
+
+ m_sentData.context.clear();
+ m_sentData.identity.clear();
+ } else {
+ // Check if the identity and/or the context has changed and if it did, send that new info to the server
+ QMutexLocker mLock(&m_sentDataMutex);
+ QReadLocker rLock(&m_positionalData.m_lock);
+
+ if (m_sentData.context != m_positionalData.m_context || m_sentData.identity != m_positionalData.m_identity ) {
+ MumbleProto::UserState mpus;
+ mpus.set_session(Global::get().uiSession);
+
+ if (m_sentData.context != m_positionalData.m_context) {
+ m_sentData.context = m_positionalData.m_context;
+ mpus.set_plugin_context(m_sentData.context.toUtf8().constData(), m_sentData.context.size());
+ }
+ if (m_sentData.identity != m_positionalData.m_identity) {
+ m_sentData.identity = m_positionalData.m_identity;
+ mpus.set_plugin_identity(m_sentData.identity.toUtf8().constData());
+ }
+
+ if (Global::get().sh) {
+ // send the message if the serverHandler is available
+ Global::get().sh->sendMessage(mpus);
+ }
+ }
+ }
+ }
+}
+
+void PluginManager::on_updatesAvailable() {
+ if (Global::get().s.bPluginAutoUpdate) {
+ m_updater.update();
+ } else {
+ m_updater.promptAndUpdate();
+ }
+}
+
+void PluginManager::checkForAvailablePositionalDataPlugin() {
+ bool performSearch = false;
+ {
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ performSearch = !m_activePositionalDataPlugin;
+ }
+
+ if (performSearch) {
+ selectActivePositionalDataPlugin();
+ }
+}
diff --git a/src/mumble/PluginManager.h b/src/mumble/PluginManager.h
new file mode 100644
index 000000000..02f4db5fa
--- /dev/null
+++ b/src/mumble/PluginManager.h
@@ -0,0 +1,279 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINMANAGER_H_
+#define MUMBLE_MUMBLE_PLUGINMANAGER_H_
+
+#include <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QTimer>
+#include <QHash>
+#include <QMutex>
+#ifdef Q_OS_WIN
+ #ifndef NOMINMAX
+ #define NOMINMAX
+ #endif
+ #include <windows.h>
+#endif
+#include "Plugin.h"
+#include "MumbleApplication.h"
+#include "PositionalData.h"
+
+#include "User.h"
+#include "ClientUser.h"
+#include "Channel.h"
+#include "Settings.h"
+#include "PluginUpdater.h"
+
+#include <functional>
+
+/// A struct for holding the values of the current context and identity that have been sent to the server
+struct PluginManager_SentData {
+ QString context;
+ QString identity;
+};
+
+
+/// The plugin manager is the central object dealing with everything plugin-related. It is responsible for
+/// finding, loading and managing the plugins. It also is responsible for invoking callback functions in the plugins
+/// and can be used by Mumble to communicate with them
+class PluginManager : public QObject {
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(PluginManager)
+ protected:
+ /// Lock for pluginHashMap. This lock has to be aquired when accessing pluginHashMap
+ mutable QReadWriteLock m_pluginCollectionLock;
+ /// A map between plugin-IDs and the actual plugin objects. You have to aquire pluginCollectionLock before
+ /// accessing this map.
+ QHash<plugin_id_t, plugin_ptr_t> m_pluginHashMap;
+ /// A set of directories to search plugins in
+ QSet<QString> m_pluginSearchPaths;
+#ifdef Q_OS_WIN
+ // This stuff is apparently needed on Windows in order to deal with DLLs
+ HANDLE m_hToken;
+ TOKEN_PRIVILEGES m_tpPrevious;
+ DWORD m_cbPrevious;
+#endif
+ /// The PositionalData object holding the current positional data (as retrieved by the respective plugin)
+ PositionalData m_positionalData;
+
+ /// A timer that causes the manager to regularly check for available plugins that can currently
+ /// deliver positional data.
+ QTimer m_positionalDataCheckTimer;
+
+ /// The mutex for sentData. This has to be aquired before accessing sentData
+ mutable QMutex m_sentDataMutex;
+ /// The bits of the positional data that have already been sent to the server. It is used to determine whether
+ /// the new data has to be sent to the server (in case it has changed). You have ti aquire sentDataMutex before
+ /// accessing this field.
+ PluginManager_SentData m_sentData;
+
+ /// The lock for activePositionalDataPlugin. It has to be aquired before accessing the respective field.
+ mutable QReadWriteLock m_activePosDataPluginLock;
+ /// The plugin that is currently used to retrieve positional data. You have to aquire activePosDataPluginLock before
+ /// accessing this field.
+ plugin_ptr_t m_activePositionalDataPlugin;
+ /// The PluginUpdater used to handle plugin updates.
+ PluginUpdater m_updater;
+
+ // We override the QObject::eventFilter function in order to be able to install the pluginManager as an event filter
+ // to the main application in order to get notified about keystrokes.
+ bool eventFilter(QObject *target, QEvent *event) Q_DECL_OVERRIDE;
+
+ /// Unloads all plugins that are currently loaded.
+ void unloadPlugins() const;
+ /// Clears the current list of plugins
+ void clearPlugins();
+ /// Iterates over the plugins and tries to select a plugin that currently claims to be able to deliver positional data. If
+ /// it found a plugin, activePositionalDataPlugin is set accordingly. If not, it is set to nullptr.
+ ///
+ /// @returns Whether this function succeeded in finding such a plugin
+ bool selectActivePositionalDataPlugin();
+
+ /// A internal helper function that iterates over all plugins and calls the given function providing the current plugin as
+ /// a parameter.
+ void foreachPlugin(std::function<void(Plugin&)>) const;
+ public:
+ // How often positional data (identity & context) should be synched with the server if there is any (in ms)
+ static constexpr int POSITIONAL_SERVER_SYNC_INTERVAL = 500;
+ // How often the manager should check for available positional data plugins
+ static constexpr int POSITIONAL_DATA_CHECK_INTERVAL = 1000;
+
+ /// Constructor
+ ///
+ /// @param additionalSearchPaths A pointer to a set of additional search paths or nullptr if no additional
+ /// paths are required.
+ /// @param p The parent QObject
+ PluginManager(QSet<QString> *additionalSearchPaths = nullptr, QObject *p = nullptr);
+ /// Destructor
+ virtual ~PluginManager() Q_DECL_OVERRIDE;
+
+ /// @param pluginID The ID of the plugin that should be retreved
+ /// @returns A pointer to the plugin with the given ID or nullptr if no such plugin could be found
+ const_plugin_ptr_t getPlugin(plugin_id_t pluginID) const;
+ /// Checks whether there are any updates for the plugins and if there are it invokes the PluginUpdater.
+ void checkForPluginUpdates();
+ /// Fetches positional data from the activePositionalDataPlugin if there is one set. This function will update the
+ /// positionalData field
+ ///
+ /// @returns Whether the positional data could be retrieved successfully
+ bool fetchPositionalData();
+ /// Unlinks the currently active positional data plugin. Effectively this sets activePositionalDataPlugin to nullptr
+ void unlinkPositionalData();
+ /// @returns Whether positional data is currently available (it has been successfully set via fetchPositionalData)
+ bool isPositionalDataAvailable() const;
+ /// @returns The most recent positional data
+ const PositionalData& getPositionalData() const;
+ /// Enables positional data gathering for the plugin with the given ID. A plugin is only even asked whether it can deliver
+ /// positional data if this is enabled.
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param enable Whether to enable positional data (alternative is to disable it)
+ void enablePositionalDataFor(plugin_id_t pluginID, bool enable = true) const;
+ /// @returns A const vector of the plugins
+ const QVector<const_plugin_ptr_t> getPlugins(bool sorted = false) const;
+ /// Loads the plugin with the given ID. Loading means initializing the plugin.
+ ///
+ /// @param pluginID The ID of the plugin to load
+ /// @returns Whether the plugin could be successfully loaded
+ bool loadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the plugin with the given ID. Unloading means shutting the plugign down.
+ ///
+ /// @param pluginID The ID of the plugin to unload
+ void unloadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the given plugin. Unloading means shutting the plugign down.
+ ///
+ /// @param plugin The plugin to unload
+ void unloadPlugin(Plugin &plugin) const;
+ /// Clears the plugin from the list of known plugins
+ ///
+ /// @param pluginID The ID of the plugin to forget about
+ /// @returns Whether the plugin has been cleared successfully
+ bool clearPlugin(plugin_id_t pluginID);
+ /// Deactivates the given features for the plugin with the given ID
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param features The feature set that should be deactivated. The features are or'ed together.
+ /// @returns The feature set that could not be deactivated
+ uint32_t deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const;
+ /// Allows or forbids the given plugin to monitor keyboard events.
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param allow Whether to allow the monitoring or not
+ void allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const;
+ /// Checks whether a plugin with the given ID exists.
+ ///
+ /// @param pluginID The ID to check
+ /// @returns Whether such a plugin exists
+ bool pluginExists(plugin_id_t pluginID) const;
+
+ public slots:
+ /// Rescans the plugin directory and load all plugins from there after having cleared the current plugin list
+ void rescanPlugins();
+ /// Slot that gets called whenever data from another plugin has been received. This function will then delegate
+ /// this to the respective plugin callback
+ ///
+ /// @param sender A pointer to the ClientUser whose client has sent the data
+ /// @param data The byte-array representing the sent data
+ /// @param dataLength The length of the data array
+ /// @param dataID The ID of the data
+ void on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const;
+ /// Slot that gets called when the local client connects to a server. It will delegate it to the respective plugin callback.
+ void on_serverConnected() const;
+ /// Slot that gets called when the local client disconnects to a server. It will delegate it to the respective plugin callback.
+ void on_serverDisconnected() const;
+ /// Slot that gets called when a client enters a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param newChannel A pointer to the new channel
+ /// @param prevChannel A pointer to the previous channel or nullptr if no such channel exists
+ /// @param user A pointer to the user that entered the channel
+ void on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const;
+ /// Slot that gets called when a client leaves a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param channel A pointer to the channel that has been left
+ /// @param user A pointer to the user that entered the channel
+ void on_channelExited(const Channel *channel, const User *user) const;
+ /// Slot that gets called when the local client changes its talking state. It will delegate it to the respective plugin callback.
+ void on_userTalkingStateChanged() const;
+ /// Slot that gets called when the local client receives audio input. It will delegate it to the respective plugin callback.
+ ///
+ /// @param inputPCM The array containing the input PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ void on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const;
+ /// Slot that gets called when the local client has fetched an audio source. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ /// @param user A pointer to the ClientUser the audio source corresposnds to
+ void on_audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech,
+ const ClientUser *user) const;
+ /// Slot that gets called when the local client is about to play some audio. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ void on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate,
+ bool *modifiedAudio) const;
+ /// Slot that gets called after the local client has finished synchronizing with the server. It will delegate it to the respective
+ /// plugin callback.
+ void on_serverSynchronized() const;
+ /// Slot that gets called when a new user is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the added user
+ void on_userAdded(unsigned int userID) const;
+ /// Slot that gets called when a user is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the removed user
+ void on_userRemoved(unsigned int userID) const;
+ /// Slot that gets called when a new channel is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the added channel
+ void on_channelAdded(int channelID) const;
+ /// Slot that gets called when a channel is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the removed channel
+ void on_channelRemoved(int channelID) const;
+ /// Slot that gets called when a channel is renamed. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the renamed channel
+ void on_channelRenamed(int channelID) const;
+ /// Slot that gets called when a key has been pressed or released while Mumble has keyboard focus.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const;
+
+ /// Slot that gets called whenever the positional data should be synchronized with the server. Before it does that, it tries to
+ /// fetch new data.
+ void on_syncPositionalData();
+ /// Slot called if there are plugin updates available
+ void on_updatesAvailable();
+
+ protected slots:
+ /// If there is no active positional data plugin, this function will initiate searching for a
+ /// new one.
+ void checkForAvailablePositionalDataPlugin();
+
+ signals:
+ /// A signal emitted if the PluginManager (acting as an event filter) detected
+ /// a QKeyEvent.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress);
+};
+
+#endif
diff --git a/src/mumble/PluginUpdater.cpp b/src/mumble/PluginUpdater.cpp
new file mode 100644
index 000000000..e7f4f2594
--- /dev/null
+++ b/src/mumble/PluginUpdater.cpp
@@ -0,0 +1,379 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginUpdater.h"
+#include "PluginManager.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtWidgets/QCheckBox>
+#include <QtWidgets/QLabel>
+#include <QtCore/QHashIterator>
+#include <QtCore/QSignalBlocker>
+#include <QtCore/QByteArray>
+#include <QtCore/QDir>
+#include <QtCore/QFile>
+#include <QtConcurrent>
+#include <QNetworkRequest>
+
+#include <algorithm>
+
+PluginUpdater::PluginUpdater(QWidget *parent)
+ : QDialog(parent),
+ m_wasInterrupted(false),
+ m_dataMutex(),
+ m_pluginsToUpdate(),
+ m_networkManager(),
+ m_pluginUpdateWidgets() {
+
+ QObject::connect(&m_networkManager, &QNetworkAccessManager::finished, this, &PluginUpdater::on_updateDownloaded);
+}
+
+PluginUpdater::~PluginUpdater() {
+ m_wasInterrupted.store(true);
+}
+
+void PluginUpdater::checkForUpdates() {
+ // Dispatch a thread in which each plugin can check for updates
+ QtConcurrent::run([this]() {
+ QMutexLocker lock(&m_dataMutex);
+
+ const QVector<const_plugin_ptr_t> plugins = Global::get().pluginManager->getPlugins();
+
+ for (int i = 0; i < plugins.size(); i++) {
+ const_plugin_ptr_t plugin = plugins[i];
+
+ if (plugin->hasUpdate()) {
+ QUrl updateURL = plugin->getUpdateDownloadURL();
+
+ if (updateURL.isValid() && !updateURL.isEmpty() && !updateURL.fileName().isEmpty()) {
+ UpdateEntry entry = { plugin->getID(), updateURL, updateURL.fileName(), 0 };
+ m_pluginsToUpdate << entry;
+ }
+ }
+
+ // if the update has been asked to be interrupted, exit here
+ if (m_wasInterrupted.load()) {
+ emit updateInterrupted();
+ return;
+ }
+ }
+
+ if (!m_pluginsToUpdate.isEmpty()) {
+ emit updatesAvailable();
+ }
+ });
+}
+
+void PluginUpdater::promptAndUpdate() {
+ setupUi(this);
+ populateUI();
+
+ setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
+
+ QObject::connect(qcbSelectAll, &QCheckBox::stateChanged, this, &PluginUpdater::on_selectAll);
+ QObject::connect(this, &QDialog::finished, this, &PluginUpdater::on_finished);
+
+ if (exec() == QDialog::Accepted) {
+ update();
+ }
+}
+
+void PluginUpdater::update() {
+ QMutexLocker l(&m_dataMutex);
+
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ UpdateEntry currentEntry = m_pluginsToUpdate[i];
+
+ // The network manager will be emit a signal once the request has finished processing.
+ // Thus we can ignore the returned QNetworkReply* here.
+ m_networkManager.get(QNetworkRequest(currentEntry.updateURL));
+ }
+}
+
+void PluginUpdater::populateUI() {
+ clearUI();
+
+ QMutexLocker l(&m_dataMutex);
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ UpdateEntry currentEntry = m_pluginsToUpdate[i];
+ plugin_id_t pluginID = currentEntry.pluginID;
+
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(pluginID);
+
+ if (!plugin) {
+ continue;
+ }
+
+ QCheckBox *checkBox = new QCheckBox(qwContent);
+ checkBox->setText(plugin->getName());
+ checkBox->setToolTip(plugin->getDescription());
+
+ checkBox->setProperty("pluginID", pluginID);
+
+ QLabel *urlLabel = new QLabel(qwContent);
+ urlLabel->setText(currentEntry.updateURL.toString());
+ urlLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ UpdateWidgetPair pair = { checkBox, urlLabel };
+ m_pluginUpdateWidgets << pair;
+
+ QObject::connect(checkBox, &QCheckBox::stateChanged, this, &PluginUpdater::on_singleSelectionChanged);
+ }
+
+ // sort the plugins alphabetically
+ std::sort(m_pluginUpdateWidgets.begin(), m_pluginUpdateWidgets.end(), [](const UpdateWidgetPair &first, const UpdateWidgetPair &second) {
+ return first.pluginCheckBox->text().compare(second.pluginCheckBox->text(), Qt::CaseInsensitive) < 0;
+ });
+
+ // add the widgets to the layout
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ static_cast<QFormLayout*>(qwContent->layout())->addRow(currentPair.pluginCheckBox, currentPair.urlLabel);
+ }
+}
+
+void PluginUpdater::clearUI() {
+ // There are always as many checkboxes as there are labels
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ qwContent->layout()->removeWidget(currentPair.pluginCheckBox);
+ qwContent->layout()->removeWidget(currentPair.urlLabel);
+
+ delete currentPair.pluginCheckBox;
+ delete currentPair.urlLabel;
+ }
+}
+
+void PluginUpdater::on_selectAll(int checkState) {
+ // failsafe for partially selected state (shouldn't happen though)
+ if (checkState == Qt::PartiallyChecked) {
+ checkState = Qt::Unchecked;
+ }
+
+ // Select or deselect all plugins
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ currentPair.pluginCheckBox->setCheckState(static_cast<Qt::CheckState>(checkState));
+ }
+}
+
+void PluginUpdater::on_singleSelectionChanged(int checkState) {
+ bool isChecked = checkState == Qt::Checked;
+
+ // Block signals for the selectAll checkBox in order to not trigger its
+ // check-logic when changing its check-state here
+ const QSignalBlocker blocker(qcbSelectAll);
+
+ if (!isChecked) {
+ // If even a single item is unchecked, the selectAll checkbox has to be unchecked
+ qcbSelectAll->setCheckState(Qt::Unchecked);
+ return;
+ }
+
+ // iterate through all checkboxes to see whether we have to toggle the selectAll checkbox
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ const UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ if (!currentPair.pluginCheckBox->isChecked()) {
+ // One unchecked checkBox is enough to know that the selectAll
+ // CheckBox can't be checked, so we can abort at this point
+ return;
+ }
+ }
+
+ qcbSelectAll->setCheckState(Qt::Checked);
+}
+
+void PluginUpdater::on_finished(int result) {
+ if (result == QDialog::Accepted) {
+ if (qcbSelectAll->isChecked()) {
+ // all plugins shall be updated, so we don't have to check them individually
+ return;
+ }
+
+ QMutexLocker l(&m_dataMutex);
+
+ // The user wants to update the selected plugins only
+ // remove the plugins that shouldn't be updated from m_pluginsToUpdate
+ auto it = m_pluginsToUpdate.begin();
+ while (it != m_pluginsToUpdate.end()) {
+ plugin_id_t id = it->pluginID;
+
+ // find the corresponding checkbox
+ bool updateCurrent = false;
+ for (int k = 0; k < m_pluginUpdateWidgets.size(); k++) {
+ QCheckBox *checkBox = m_pluginUpdateWidgets[k].pluginCheckBox;
+ QVariant idVariant = checkBox->property("pluginID");
+
+ if (idVariant.isValid() && static_cast<plugin_id_t>(idVariant.toInt()) == id) {
+ updateCurrent = checkBox->isChecked();
+ break;
+ }
+ }
+
+ if (!updateCurrent) {
+ // remove this entry from the update-vector
+ it = m_pluginsToUpdate.erase(it);
+ } else {
+ it++;
+ }
+ }
+ } else {
+ // Nothing to do as the user doesn't want to update anyways
+ }
+}
+
+void PluginUpdater::interrupt() {
+ m_wasInterrupted.store(true);
+}
+
+void PluginUpdater::on_updateDownloaded(QNetworkReply *reply) {
+ if (reply) {
+ // Schedule reply for deletion
+ reply->deleteLater();
+
+ if (m_wasInterrupted.load()) {
+ emit updateInterrupted();
+ return;
+ }
+
+ // Find the ID of the plugin this update is for by comparing the URLs
+ UpdateEntry entry;
+ bool foundID = false;
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ if (m_pluginsToUpdate[i].updateURL == reply->url()) {
+ foundID = true;
+
+ // remove that entry from the vector as it is being updated right here
+ entry = m_pluginsToUpdate.takeAt(i);
+ break;
+ }
+ }
+ }
+
+ if (!foundID) {
+ // Can't match the URL to a pluginID
+ qWarning() << "PluginUpdater: Requested update for plugin from"
+ << reply->url() << "but didn't find corresponding plugin again!";
+ return;
+ }
+
+ // Now get a handle to that plugin
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(entry.pluginID);
+
+ if (!plugin) {
+ // Can't find plugin with given ID
+ qWarning() << "PluginUpdater: Got update for plugin with id"
+ << entry.pluginID << "but it doesn't seem to exist anymore!";
+ return;
+ }
+
+ // We can start actually checking the reply here
+ if (reply->error() != QNetworkReply::NoError) {
+ // There was an error during this request. Report it
+ Log::logOrDefer(Log::Warning,
+ tr("Unable to download plugin update for \"%1\" from \"%2\" (%3)").arg(
+ plugin->getName()).arg(reply->url().toString()).arg(
+ QString::fromLatin1(
+ QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(reply->error())
+ )
+ )
+ );
+ return;
+ }
+
+ // Check HTTP status code (just because the request was successful, doesn't
+ // mean the data was downloaded successfully
+ int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (httpStatusCode >= 300 && httpStatusCode < 400) {
+ // We have been redirected
+ if (entry.redirects >= MAX_REDIRECTS - 1) {
+ // Maximum redirect count exceeded
+ Log::logOrDefer(Log::Warning, tr("Update for plugin \"%1\" failed due to too many redirects").arg(plugin->getName()));
+
+ return;
+ }
+
+ QUrl redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ // Because the redirection url can be relative,
+ // we have to use the previous one to resolve it
+ redirectedUrl = reply->url().resolved(redirectedUrl);
+
+ // Re-insert the current plugin into the list of updating plugins (using the
+ // new URL so that it will be associated with that instead of the old one)
+ entry.updateURL = redirectedUrl;
+ entry.redirects++;
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ m_pluginsToUpdate << entry;
+ }
+
+ // Post a new request for the file to the new URL
+ m_networkManager.get(QNetworkRequest(redirectedUrl));
+
+ return;
+ }
+
+ if (httpStatusCode < 200 || httpStatusCode >= 300) {
+ // HTTP request has failed
+ Log::logOrDefer(Log::Warning,
+ tr("Unable to download plugin update for \"%1\" from \"%2\" (HTTP status code %3)").arg(
+ plugin->getName()).arg(reply->url().toString()).arg(httpStatusCode)
+ );
+
+ return;
+ }
+
+ // Reply seems fine -> write file to disk and fire installer
+ QByteArray content = reply->readAll();
+
+ // Write the content to a file in the temp-dir
+ if (content.isEmpty()) {
+ qWarning() << "PluginUpdater: Update for" << plugin->getName() << "from"
+ << reply->url().toString() << "resulted in no content!";
+ return;
+ }
+
+ QFile file(QDir::temp().filePath(entry.fileName));
+ if (!file.open(QIODevice::WriteOnly)) {
+ qWarning() << "PluginUpdater: Can't open" << file.fileName() << "for writing!";
+ return;
+ }
+
+ file.write(content);
+ file.close();
+
+ try {
+ // Launch installer
+ PluginInstaller installer(QFileInfo(file.fileName()));
+ installer.install();
+
+ Log::logOrDefer(Log::Information, tr("Successfully updated plugin \"%1\"").arg(plugin->getName()));
+
+ // Make sure Mumble won't use the old version of the plugin
+ Global::get().pluginManager->rescanPlugins();
+ } catch (const PluginInstallException &e) {
+ Log::logOrDefer(Log::CriticalError, e.getMessage());
+ }
+
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ if (m_pluginsToUpdate.isEmpty()) {
+ emit updatingFinished();
+ }
+ }
+ }
+}
diff --git a/src/mumble/PluginUpdater.h b/src/mumble/PluginUpdater.h
new file mode 100644
index 000000000..2d9041d3d
--- /dev/null
+++ b/src/mumble/PluginUpdater.h
@@ -0,0 +1,107 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINUPDATER_H_
+#define MUMBLE_MUMBLE_PLUGINUPDATER_H_
+
+#include <QtCore/QVector>
+#include <QtCore/QUrl>
+#include <QtCore/QMutex>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include <atomic>
+
+#include "ui_PluginUpdater.h"
+#include "Plugin.h"
+
+/// A helper struct to store a pair of a CheckBox and a label corresponding to
+/// a single plugin.
+struct UpdateWidgetPair {
+ QCheckBox *pluginCheckBox;
+ QLabel *urlLabel;
+};
+
+/// A helper struct to store a pair of a plugin ID and an URL corresponding to
+/// the same plugin.
+struct UpdateEntry {
+ plugin_id_t pluginID;
+ QUrl updateURL;
+ QString fileName;
+ int redirects;
+};
+
+/// A class designed for managing plugin updates. At the same time this also represents
+/// a Dialog that can be used to prompt the user whether certain updates should be updated.
+class PluginUpdater : public QDialog, public Ui::PluginUpdater {
+ private:
+ Q_OBJECT;
+ Q_DISABLE_COPY(PluginUpdater);
+
+ protected:
+ /// An atomic flag indicating whether the plugin update has been interrupted. It is used
+ /// to exit some loops in different threads before they are done.
+ std::atomic<bool> m_wasInterrupted;
+ /// A mutex for m_pluginsToUpdate.
+ QMutex m_dataMutex;
+ /// A vector holding plugins that can be updated by storing a pluginID and the download URL
+ /// in form of an UpdateEntry.
+ QVector<UpdateEntry> m_pluginsToUpdate;
+ /// The NetworkManager used to perform the downloding of plugins.
+ QNetworkAccessManager m_networkManager;
+ /// A vector of the UI elements created for the individual plugins (in form of UpdateWidgetPairs).
+ /// NOTE: This vector may only be accessed from the UI thread this dialog is living in!
+ QVector<UpdateWidgetPair> m_pluginUpdateWidgets;
+
+ /// Populates the UI with plugins that have been found to have an update available (through a call
+ /// to checkForUpdates()).
+ void populateUI();
+
+ public:
+ /// Constructor
+ ///
+ /// @param parent A pointer to the QWidget parent of this object
+ PluginUpdater(QWidget *parent = nullptr);
+ /// Destructor
+ ~PluginUpdater();
+
+ // The maximum number of redirects to allow
+ static constexpr int MAX_REDIRECTS = 10;
+
+ /// Triggers an update check for all plugins that are currently recognized by Mumble. This is done
+ /// in a non-blocking fashion (in another thread). Once all plugins have been checked and if there
+ /// are updates available, the updatesAvailable signal is emitted.
+ void checkForUpdates();
+ /// Launches a Dialog that asks the user which of the plugins an update has been found for, shall be
+ /// updated. If the user has selected at least selected one plugin and has accepted the dialog, this
+ /// function will automatically call update().
+ void promptAndUpdate();
+ /// Starts the update process of the plugins. This is done asynchronously.
+ void update();
+ public slots:
+ /// Clears the UI from the widgets created for the individual plugins.
+ void clearUI();
+ /// Slot triggered if the user changes the state of the selectAll CheckBox.
+ void on_selectAll(int checkState);
+ /// Slot triggered if the user toggles the CheckBox for any individual plugin.
+ void on_singleSelectionChanged(int checkState);
+ /// Slot triggered when the dialog is being closed.
+ void on_finished(int result);
+ /// Slot that can be triggered to ask for the update process to be interrupted.
+ void interrupt();
+ protected slots:
+ /// Slot triggered once an update for a plugin has been downloaded.
+ void on_updateDownloaded(QNetworkReply *reply);
+
+ signals:
+ /// This signal is emitted once it has been determined that there are plugin updates available.
+ void updatesAvailable();
+ /// This signal is emitted once all plugin updates have been downloaded and processed.
+ void updatingFinished();
+ /// This signal is emitted every time the update process has been interrupted.
+ void updateInterrupted();
+};
+
+#endif // MUMBLE_MUMBLE_PLUGINUPDATER_H_
diff --git a/src/mumble/PluginUpdater.ui b/src/mumble/PluginUpdater.ui
new file mode 100644
index 000000000..8f2118d65
--- /dev/null
+++ b/src/mumble/PluginUpdater.ui
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginUpdater</class>
+ <widget class="QDialog" name="PluginUpdater">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>616</width>
+ <height>460</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginUpdater</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlTitleText">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>The following plugins can be updated.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbSelectAll">
+ <property name="text">
+ <string>Select all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwContent">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>600</width>
+ <height>284</height>
+ </rect>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="horizontalSpacing">
+ <number>15</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlPlugin">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Plugin</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlURL">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Download-URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="qlUpdateSelected">
+ <property name="text">
+ <string>Do you want to update the selected plugins?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/mumble/Plugins.cpp b/src/mumble/Plugins.cpp
deleted file mode 100644
index 4510f757c..000000000
--- a/src/mumble/Plugins.cpp
+++ /dev/null
@@ -1,792 +0,0 @@
-// Copyright 2007-2021 The Mumble Developers. All rights reserved.
-// Use of this source code is governed by a BSD-style license
-// that can be found in the LICENSE file at the root of the
-// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-
-#include "Plugins.h"
-
-#include "../../plugins/mumble_plugin.h"
-#include "Log.h"
-#include "MainWindow.h"
-#include "Message.h"
-#include "MumbleApplication.h"
-#include "ServerHandler.h"
-#include "Utils.h"
-#include "WebFetch.h"
-#ifdef USE_MANUAL_PLUGIN
-# include "ManualPlugin.h"
-#endif
-#include "Global.h"
-
-#include <QtCore/QLibrary>
-#include <QtCore/QUrlQuery>
-
-#ifdef Q_OS_WIN
-# include <QtCore/QTemporaryFile>
-#endif
-
-#include <QtWidgets/QMessageBox>
-#include <QtXml/QDomDocument>
-
-#ifdef Q_OS_WIN
-# include <softpub.h>
-# include <tlhelp32.h>
-#endif
-
-const QString PluginConfig::name = QLatin1String("PluginConfig");
-
-static ConfigWidget *PluginConfigDialogNew(Settings &st) {
- return new PluginConfig(st);
-}
-
-static ConfigRegistrar registrarPlugins(5000, PluginConfigDialogNew);
-
-struct PluginInfo {
- bool locked;
- bool enabled;
- QLibrary lib;
- QString filename;
- QString description;
- QString shortname;
- MumblePlugin *p;
- MumblePlugin2 *p2;
- MumblePluginQt *pqt;
- PluginInfo();
-};
-
-PluginInfo::PluginInfo() {
- locked = false;
- enabled = false;
- p = nullptr;
- p2 = nullptr;
- pqt = nullptr;
-}
-
-struct PluginFetchMeta {
- QString hash;
- QString path;
-
- PluginFetchMeta(const QString &hash_ = QString(), const QString &path_ = QString())
- : hash(hash_), path(path_) { /* Empty */
- }
-};
-
-
-PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) {
- setupUi(this);
- qtwPlugins->setAccessibleName(tr("Plugins"));
- qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch);
- qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
-
- refillPluginList();
-}
-
-QString PluginConfig::title() const {
- return tr("Plugins");
-}
-
-const QString &PluginConfig::getName() const {
- return PluginConfig::name;
-}
-
-QIcon PluginConfig::icon() const {
- return QIcon(QLatin1String("skin:config_plugin.png"));
-}
-
-void PluginConfig::load(const Settings &r) {
- loadCheckBox(qcbTransmit, r.bTransmitPosition);
-}
-
-void PluginConfig::save() const {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
-
- s.bTransmitPosition = qcbTransmit->isChecked();
- s.qmPositionalAudioPlugins.clear();
-
- QList< QTreeWidgetItem * > list = qtwPlugins->findItems(QString(), Qt::MatchContains);
- foreach (QTreeWidgetItem *i, list) {
- bool enabled = (i->checkState(1) == Qt::Checked);
-
- PluginInfo *pi = pluginForItem(i);
- if (pi) {
- s.qmPositionalAudioPlugins.insert(pi->filename, enabled);
- pi->enabled = enabled;
- }
- }
-}
-
-PluginInfo *PluginConfig::pluginForItem(QTreeWidgetItem *i) const {
- if (i) {
- foreach (PluginInfo *pi, Global::get().p->qlPlugins) {
- if (pi->filename == i->data(0, Qt::UserRole).toString())
- return pi;
- }
- }
- return nullptr;
-}
-
-void PluginConfig::on_qpbConfig_clicked() {
- PluginInfo *pi;
- {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- pi = pluginForItem(qtwPlugins->currentItem());
- }
-
- if (!pi)
- return;
-
- if (pi->pqt && pi->pqt->config) {
- pi->pqt->config(this);
- } else if (pi->p->config) {
- pi->p->config(0);
- } else {
- QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."),
- QMessageBox::Ok, QMessageBox::NoButton);
- }
-}
-
-void PluginConfig::on_qpbAbout_clicked() {
- PluginInfo *pi;
- {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- pi = pluginForItem(qtwPlugins->currentItem());
- }
-
- if (!pi)
- return;
-
- if (pi->pqt && pi->pqt->about) {
- pi->pqt->about(this);
- } else if (pi->p->about) {
- pi->p->about(0);
- } else {
- QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok,
- QMessageBox::NoButton);
- }
-}
-
-void PluginConfig::on_qpbReload_clicked() {
- Global::get().p->rescanPlugins();
- refillPluginList();
-}
-
-void PluginConfig::refillPluginList() {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- qtwPlugins->clear();
-
- foreach (PluginInfo *pi, Global::get().p->qlPlugins) {
- QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins);
- i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
- i->setCheckState(1, pi->enabled ? Qt::Checked : Qt::Unchecked);
- i->setText(0, pi->description);
- if (pi->p->longdesc)
- i->setToolTip(0, QString::fromStdWString(pi->p->longdesc()).toHtmlEscaped());
- i->setData(0, Qt::UserRole, pi->filename);
- }
- qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0));
- on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), nullptr);
-}
-
-void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
-
- PluginInfo *pi = pluginForItem(current);
- if (pi) {
- bool showAbout = false;
- if (pi->p->about) {
- showAbout = true;
- }
- if (pi->pqt && pi->pqt->about) {
- showAbout = true;
- }
- qpbAbout->setEnabled(showAbout);
-
- bool showConfig = false;
- if (pi->p->config) {
- showConfig = true;
- }
- if (pi->pqt && pi->pqt->config) {
- showConfig = true;
- }
- qpbConfig->setEnabled(showConfig);
- } else {
- qpbAbout->setEnabled(false);
- qpbConfig->setEnabled(false);
- }
-}
-
-Plugins::Plugins(QObject *p) : QObject(p) {
- QTimer *timer = new QTimer(this);
- timer->setObjectName(QLatin1String("Timer"));
- timer->start(500);
- locked = prevlocked = nullptr;
- bValid = false;
- iPluginTry = 0;
- for (int i = 0; i < 3; i++)
- fPosition[i] = fFront[i] = fTop[i] = 0.0;
- QMetaObject::connectSlotsByName(this);
-
-#ifdef QT_NO_DEBUG
-# ifndef MUMBLE_PLUGIN_PATH
- qsSystemPlugins =
- QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath());
-# ifdef Q_OS_MAC
- qsSystemPlugins = QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath());
-# endif
-# else
- qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH));
-# endif
-
- qsUserPlugins = Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins");
-#else
-# ifdef MUMBLE_PLUGIN_PATH
- qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH));
-# else
- qsSystemPlugins = QString();
-# endif
-
- qsUserPlugins = QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath());
-#endif
-
-#ifdef Q_OS_WIN
- // According to MS KB Q131065, we need this to OpenProcess()
-
- hToken = nullptr;
-
- if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken)) {
- if (GetLastError() == ERROR_NO_TOKEN) {
- ImpersonateSelf(SecurityImpersonation);
- OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken);
- }
- }
-
- TOKEN_PRIVILEGES tp;
- LUID luid;
- cbPrevious = sizeof(TOKEN_PRIVILEGES);
-
- LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &luid);
-
- tp.PrivilegeCount = 1;
- tp.Privileges[0].Luid = luid;
- tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
-
- AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &tpPrevious, &cbPrevious);
-#endif
-}
-
-Plugins::~Plugins() {
- clearPlugins();
-
-#ifdef Q_OS_WIN
- AdjustTokenPrivileges(hToken, FALSE, &tpPrevious, cbPrevious, nullptr, nullptr);
- CloseHandle(hToken);
-#endif
-}
-
-void Plugins::clearPlugins() {
- QWriteLocker lock(&Global::get().p->qrwlPlugins);
- foreach (PluginInfo *pi, qlPlugins) {
- if (pi->locked)
- pi->p->unlock();
- pi->lib.unload();
- delete pi;
- }
- qlPlugins.clear();
-}
-
-void Plugins::rescanPlugins() {
- clearPlugins();
-
- QWriteLocker lock(&Global::get().p->qrwlPlugins);
- prevlocked = locked = nullptr;
- bValid = false;
-
- QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QDir qud(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QFileInfoList libs = qud.entryInfoList() + qd.entryInfoList();
-
- QSet< QString > evaluated;
- foreach (const QFileInfo &libinfo, libs) {
- QString fname = libinfo.fileName();
- QString libname = libinfo.absoluteFilePath();
- if (!evaluated.contains(fname) && QLibrary::isLibrary(libname)) {
- PluginInfo *pi = new PluginInfo();
- pi->lib.setFileName(libname);
- pi->filename = fname;
- if (pi->lib.load()) {
- mumblePluginFunc mpf = reinterpret_cast< mumblePluginFunc >(pi->lib.resolve("getMumblePlugin"));
- if (mpf) {
- evaluated.insert(fname);
- pi->p = mpf();
-
- // Check whether the plugin has a valid plugin magic and that it's not retracted.
- // A retracted plugin is a plugin that clients should disregard, typically because
- // the game the plugin was written for now provides positional audio via the
- // link plugin (see null_plugin.cpp).
- if (pi->p && pi->p->magic == MUMBLE_PLUGIN_MAGIC && pi->p->shortname != L"Retracted") {
- pi->description = QString::fromStdWString(pi->p->description);
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true);
-
- mumblePlugin2Func mpf2 =
- reinterpret_cast< mumblePlugin2Func >(pi->lib.resolve("getMumblePlugin2"));
- if (mpf2) {
- pi->p2 = mpf2();
- if (pi->p2->magic != MUMBLE_PLUGIN_MAGIC_2) {
- pi->p2 = nullptr;
- }
- }
-
- mumblePluginQtFunc mpfqt =
- reinterpret_cast< mumblePluginQtFunc >(pi->lib.resolve("getMumblePluginQt"));
- if (mpfqt) {
- pi->pqt = mpfqt();
- if (pi->pqt->magic != MUMBLE_PLUGIN_MAGIC_QT) {
- pi->pqt = nullptr;
- }
- }
-
- qlPlugins << pi;
- continue;
- }
- }
- pi->lib.unload();
- } else {
- qWarning("Plugins: Failed to load %s: %s", qPrintable(pi->filename), qPrintable(pi->lib.errorString()));
- }
- delete pi;
- }
- }
-
- // Handle built-in plugins
- {
-#if defined(USE_MANUAL_PLUGIN)
- // Manual plugin
- PluginInfo *pi = new PluginInfo();
- pi->filename = QLatin1String("manual.builtin");
- pi->p = ManualPlugin_getMumblePlugin();
- pi->pqt = ManualPlugin_getMumblePluginQt();
- pi->description = QString::fromStdWString(pi->p->description);
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true);
- qlPlugins << pi;
-#endif
- }
-}
-
-bool Plugins::fetch() {
- if (Global::get().bPosTest) {
- fPosition[0] = fPosition[1] = fPosition[2] = 0.0f;
- fFront[0] = 0.0f;
- fFront[1] = 0.0f;
- fFront[2] = 1.0f;
- fTop[0] = 0.0f;
- fTop[1] = 1.0f;
- fTop[2] = 0.0f;
-
- for (int i = 0; i < 3; ++i) {
- fCameraPosition[i] = fPosition[i];
- fCameraFront[i] = fFront[i];
- fCameraTop[i] = fTop[i];
- }
-
- bValid = true;
- return true;
- }
-
- if (!locked) {
- bValid = false;
- return bValid;
- }
-
- QReadLocker lock(&qrwlPlugins);
- if (!locked) {
- bValid = false;
- return bValid;
- }
-
- if (!locked->enabled)
- bUnlink = true;
-
- bool ok;
- {
- QMutexLocker mlock(&qmPluginStrings);
- ok = locked->p->fetch(fPosition, fFront, fTop, fCameraPosition, fCameraFront, fCameraTop, ssContext,
- swsIdentity);
- }
- if (!ok || bUnlink) {
- lock.unlock();
- QWriteLocker wlock(&qrwlPlugins);
-
- if (locked) {
- locked->p->unlock();
- locked->locked = false;
- prevlocked = locked;
- locked = nullptr;
- for (int i = 0; i < 3; i++)
- fPosition[i] = fFront[i] = fTop[i] = fCameraPosition[i] = fCameraFront[i] = fCameraTop[i] = 0.0f;
- }
- }
- bValid = ok;
- return bValid;
-}
-
-void Plugins::on_Timer_timeout() {
- fetch();
-
- QReadLocker lock(&qrwlPlugins);
-
- if (prevlocked) {
- Global::get().l->log(Log::Information, tr("%1 lost link.").arg(prevlocked->shortname.toHtmlEscaped()));
- prevlocked = nullptr;
- }
-
-
- {
- QMutexLocker mlock(&qmPluginStrings);
-
- if (!locked) {
- ssContext.clear();
- swsIdentity.clear();
- }
-
- std::string context;
- if (locked)
- context.assign(u8(QString::fromStdWString(locked->p->shortname)) + static_cast< char >(0) + ssContext);
-
- if (!Global::get().uiSession) {
- ssContextSent.clear();
- swsIdentitySent.clear();
- } else if ((context != ssContextSent) || (swsIdentity != swsIdentitySent)) {
- MumbleProto::UserState mpus;
- mpus.set_session(Global::get().uiSession);
- if (context != ssContextSent) {
- ssContextSent.assign(context);
- mpus.set_plugin_context(context);
- }
- if (swsIdentity != swsIdentitySent) {
- swsIdentitySent.assign(swsIdentity);
- mpus.set_plugin_identity(u8(QString::fromStdWString(swsIdentitySent)));
- }
- if (Global::get().sh)
- Global::get().sh->sendMessage(mpus);
- }
- }
-
- if (locked) {
- return;
- }
-
- if (!Global::get().s.bTransmitPosition)
- return;
-
- lock.unlock();
- QWriteLocker wlock(&qrwlPlugins);
-
- if (qlPlugins.isEmpty())
- return;
-
- ++iPluginTry;
- if (iPluginTry >= qlPlugins.count())
- iPluginTry = 0;
-
- std::multimap< std::wstring, unsigned long long int > pids;
-#if defined(Q_OS_WIN)
- PROCESSENTRY32 pe;
-
- pe.dwSize = sizeof(pe);
- HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- if (hSnap != INVALID_HANDLE_VALUE) {
- BOOL ok = Process32First(hSnap, &pe);
-
- while (ok) {
- pids.insert(
- std::pair< std::wstring, unsigned long long int >(std::wstring(pe.szExeFile), pe.th32ProcessID));
- ok = Process32Next(hSnap, &pe);
- }
- CloseHandle(hSnap);
- }
-#elif defined(Q_OS_LINUX)
- QDir d(QLatin1String("/proc"));
- QStringList entries = d.entryList();
- bool ok;
- foreach (const QString &entry, entries) {
- // Check if the entry is a PID
- // by checking whether it's a number.
- // If it is not, skip it.
- unsigned long long int pid = static_cast< unsigned long long int >(entry.toLongLong(&ok, 10));
- if (!ok) {
- continue;
- }
-
- QString exe = QFile::symLinkTarget(QString(QLatin1String("/proc/%1/exe")).arg(entry));
- QFileInfo fi(exe);
- QString firstPart = fi.baseName();
- QString completeSuffix = fi.completeSuffix();
- QString baseName;
- if (completeSuffix.isEmpty()) {
- baseName = firstPart;
- } else {
- baseName = firstPart + QLatin1String(".") + completeSuffix;
- }
-
- if (baseName == QLatin1String("wine-preloader") || baseName == QLatin1String("wine64-preloader")) {
- QFile f(QString(QLatin1String("/proc/%1/cmdline")).arg(entry));
- if (f.open(QIODevice::ReadOnly)) {
- QByteArray cmdline = f.readAll();
- f.close();
-
- int nul = cmdline.indexOf('\0');
- if (nul != -1) {
- cmdline.truncate(nul);
- }
-
- QString exe = QString::fromUtf8(cmdline);
- if (exe.contains(QLatin1String("\\"))) {
- int lastBackslash = exe.lastIndexOf(QLatin1String("\\"));
- if (exe.count() > lastBackslash + 1) {
- baseName = exe.mid(lastBackslash + 1);
- }
- }
- }
- }
-
- if (!baseName.isEmpty()) {
- pids.insert(std::pair< std::wstring, unsigned long long int >(baseName.toStdWString(), pid));
- }
- }
-#endif
-
- PluginInfo *pi = qlPlugins.at(iPluginTry);
- if (pi->enabled) {
- if (pi->p2 ? pi->p2->trylock(pids) : pi->p->trylock()) {
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- Global::get().l->log(Log::Information, tr("%1 linked.").arg(pi->shortname.toHtmlEscaped()));
- pi->locked = true;
- bUnlink = false;
- locked = pi;
- }
- }
-}
-
-void Plugins::checkUpdates() {
- QUrl url;
- url.setPath(QLatin1String("/v1/pa-plugins"));
-
- QList< QPair< QString, QString > > queryItems;
- queryItems << qMakePair(QString::fromUtf8("ver"),
- QString::fromUtf8(QUrl::toPercentEncoding(QString::fromUtf8(MUMBLE_RELEASE))));
-#if defined(Q_OS_WIN)
-# if defined(Q_OS_WIN64)
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("WinX64"));
-# else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Win32"));
-# endif
- queryItems << qMakePair(QString::fromUtf8("abi"), QString::fromUtf8(MUMTEXT(_MSC_VER)));
-#elif defined(Q_OS_MAC)
-# if defined(USE_MAC_UNIVERSAL)
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX-Universal"));
-# else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX"));
-# endif
-#else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Unix"));
-#endif
-
-
-#ifdef QT_NO_DEBUG
- QUrlQuery query;
- query.setQueryItems(queryItems);
- url.setQuery(query);
-
- WebFetch::fetch(QLatin1String("update"), url, this, SLOT(fetchedUpdatePAPlugins(QByteArray, QUrl)));
-#else
- Global::get().mw->msgBox(tr("Skipping plugin update in debug mode."));
-#endif
-}
-
-void Plugins::fetchedUpdatePAPlugins(QByteArray data, QUrl) {
- if (data.isNull())
- return;
-
- bool rescan = false;
- qmPluginFetchMeta.clear();
- QDomDocument doc;
- doc.setContent(data);
-
- QDomElement root = doc.documentElement();
- QDomNode n = root.firstChild();
- while (!n.isNull()) {
- QDomElement e = n.toElement();
- if (!e.isNull()) {
- if (e.tagName() == QLatin1String("plugin")) {
- QString name = QFileInfo(e.attribute(QLatin1String("name"))).fileName();
- QString hash = e.attribute(QLatin1String("hash"));
- QString path = e.attribute(QLatin1String("path"));
- qmPluginFetchMeta.insert(name, PluginFetchMeta(hash, path));
- }
- }
- n = n.nextSibling();
- }
-
- QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QDir qdu(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
-
- QFileInfoList libs = qd.entryInfoList();
- foreach (const QFileInfo &libinfo, libs) {
- QString libname = libinfo.absoluteFilePath();
- QString filename = libinfo.fileName();
- PluginFetchMeta pfm = qmPluginFetchMeta.value(filename);
- QString wanthash = pfm.hash;
- if (!wanthash.isNull() && QLibrary::isLibrary(libname)) {
- QFile f(libname);
- if (wanthash.isEmpty()) {
- // Outdated plugin
- if (f.exists()) {
- clearPlugins();
- f.remove();
- rescan = true;
- }
- } else if (f.open(QIODevice::ReadOnly)) {
- QString h = QLatin1String(sha1(f.readAll()).toHex());
- f.close();
- if (h == wanthash) {
- if (qd != qdu) {
- QFile qfuser(qsUserPlugins + QString::fromLatin1("/") + filename);
- if (qfuser.exists()) {
- clearPlugins();
- qfuser.remove();
- rescan = true;
- }
- }
- // Mark for removal from userplugins
- qmPluginFetchMeta.insert(filename, PluginFetchMeta());
- }
- }
- }
- }
-
- if (qd != qdu) {
- libs = qdu.entryInfoList();
- foreach (const QFileInfo &libinfo, libs) {
- QString libname = libinfo.absoluteFilePath();
- QString filename = libinfo.fileName();
- PluginFetchMeta pfm = qmPluginFetchMeta.value(filename);
- QString wanthash = pfm.hash;
- if (!wanthash.isNull() && QLibrary::isLibrary(libname)) {
- QFile f(libname);
- if (wanthash.isEmpty()) {
- // Outdated plugin
- if (f.exists()) {
- clearPlugins();
- f.remove();
- rescan = true;
- }
- } else if (f.open(QIODevice::ReadOnly)) {
- QString h = QLatin1String(sha1(f.readAll()).toHex());
- f.close();
- if (h == wanthash) {
- qmPluginFetchMeta.remove(filename);
- }
- }
- }
- }
- }
- QMap< QString, PluginFetchMeta >::const_iterator i;
- for (i = qmPluginFetchMeta.constBegin(); i != qmPluginFetchMeta.constEnd(); ++i) {
- PluginFetchMeta pfm = i.value();
- if (!pfm.hash.isEmpty()) {
- QUrl pluginDownloadUrl;
- if (pfm.path.isEmpty()) {
- pluginDownloadUrl.setPath(QString::fromLatin1("%1").arg(i.key()));
- } else {
- pluginDownloadUrl.setPath(pfm.path);
- }
-
- WebFetch::fetch(QLatin1String("pa-plugin-dl"), pluginDownloadUrl, this,
- SLOT(fetchedPAPluginDL(QByteArray, QUrl)));
- }
- }
-
- if (rescan)
- rescanPlugins();
-}
-
-void Plugins::fetchedPAPluginDL(QByteArray data, QUrl url) {
- if (data.isNull())
- return;
-
- bool rescan = false;
-
- const QString &urlPath = url.path();
- QString fname = QFileInfo(urlPath).fileName();
- if (qmPluginFetchMeta.contains(fname)) {
- PluginFetchMeta pfm = qmPluginFetchMeta.value(fname);
- if (pfm.hash == QLatin1String(sha1(data).toHex())) {
- bool verified = true;
-#ifdef Q_OS_WIN
- verified = false;
- QString tempname;
- std::wstring tempnative;
- {
- QTemporaryFile temp(QDir::tempPath() + QLatin1String("/plugin_XXXXXX.dll"));
- if (temp.open()) {
- tempname = temp.fileName();
- tempnative = QDir::toNativeSeparators(tempname).toStdWString();
- temp.write(data);
- temp.setAutoRemove(false);
- }
- }
- if (!tempname.isNull()) {
- WINTRUST_FILE_INFO file;
- ZeroMemory(&file, sizeof(file));
- file.cbStruct = sizeof(file);
- file.pcwszFilePath = tempnative.c_str();
-
- WINTRUST_DATA data;
- ZeroMemory(&data, sizeof(data));
- data.cbStruct = sizeof(data);
- data.dwUIChoice = WTD_UI_NONE;
- data.fdwRevocationChecks = WTD_REVOKE_NONE;
- data.dwUnionChoice = WTD_CHOICE_FILE;
- data.pFile = &file;
- data.dwProvFlags = WTD_SAFER_FLAG | WTD_USE_DEFAULT_OSVER_CHECK;
- data.dwUIContext = WTD_UICONTEXT_INSTALL;
-
- static GUID guid = WINTRUST_ACTION_GENERIC_VERIFY_V2;
-
- LONG ts = WinVerifyTrust(0, &guid, &data);
-
- QFile deltemp(tempname);
- deltemp.remove();
- verified = (ts == 0);
- }
-#endif
- if (verified) {
- clearPlugins();
-
- QFile f;
- f.setFileName(qsSystemPlugins + QLatin1String("/") + fname);
- if (f.open(QIODevice::WriteOnly)) {
- f.write(data);
- f.close();
- Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- } else {
- f.setFileName(qsUserPlugins + QLatin1String("/") + fname);
- if (f.open(QIODevice::WriteOnly)) {
- f.write(data);
- f.close();
- Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- } else {
- Global::get().mw->msgBox(tr("Failed to install new plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- }
- }
-
- rescan = true;
- }
- }
- }
-
- if (rescan)
- rescanPlugins();
-}
diff --git a/src/mumble/Plugins.h b/src/mumble/Plugins.h
deleted file mode 100644
index fd951bb73..000000000
--- a/src/mumble/Plugins.h
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2007-2021 The Mumble Developers. All rights reserved.
-// Use of this source code is governed by a BSD-style license
-// that can be found in the LICENSE file at the root of the
-// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-
-#ifndef MUMBLE_MUMBLE_PLUGINS_H_
-#define MUMBLE_MUMBLE_PLUGINS_H_
-
-#include "ConfigDialog.h"
-
-#include "ui_Plugins.h"
-
-#ifdef Q_OS_WIN
-# include "win.h"
-#endif
-
-#include <QtCore/QMutex>
-#include <QtCore/QObject>
-#include <QtCore/QReadWriteLock>
-#include <QtCore/QUrl>
-
-struct PluginInfo;
-
-class PluginConfig : public ConfigWidget, public Ui::PluginConfig {
-private:
- Q_OBJECT
- Q_DISABLE_COPY(PluginConfig)
-protected:
- void refillPluginList();
- PluginInfo *pluginForItem(QTreeWidgetItem *) const;
-
-public:
- /// The unique name of this ConfigWidget
- static const QString name;
- PluginConfig(Settings &st);
- virtual QString title() const Q_DECL_OVERRIDE;
- const QString &getName() const Q_DECL_OVERRIDE;
- virtual QIcon icon() const Q_DECL_OVERRIDE;
-public slots:
- void save() const Q_DECL_OVERRIDE;
- void load(const Settings &r) Q_DECL_OVERRIDE;
- void on_qpbConfig_clicked();
- void on_qpbAbout_clicked();
- void on_qpbReload_clicked();
- void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *);
-};
-
-struct PluginFetchMeta;
-
-class Plugins : public QObject {
- friend class PluginConfig;
-
-private:
- Q_OBJECT
- Q_DISABLE_COPY(Plugins)
-protected:
- QReadWriteLock qrwlPlugins;
- QMutex qmPluginStrings;
- QList< PluginInfo * > qlPlugins;
- PluginInfo *locked;
- PluginInfo *prevlocked;
- void clearPlugins();
- int iPluginTry;
- QMap< QString, PluginFetchMeta > qmPluginFetchMeta;
- QString qsSystemPlugins;
- QString qsUserPlugins;
-#ifdef Q_OS_WIN
- HANDLE hToken;
- TOKEN_PRIVILEGES tpPrevious;
- DWORD cbPrevious;
-#endif
-public:
- std::string ssContext, ssContextSent;
- std::wstring swsIdentity, swsIdentitySent;
- bool bValid;
- bool bUnlink;
- float fPosition[3], fFront[3], fTop[3];
- float fCameraPosition[3], fCameraFront[3], fCameraTop[3];
-
- Plugins(QObject *p = nullptr);
- ~Plugins() Q_DECL_OVERRIDE;
-public slots:
- void on_Timer_timeout();
- void rescanPlugins();
- bool fetch();
- void checkUpdates();
- void fetchedUpdatePAPlugins(QByteArray, QUrl);
- void fetchedPAPluginDL(QByteArray, QUrl);
-};
-
-#endif
diff --git a/src/mumble/PositionalData.cpp b/src/mumble/PositionalData.cpp
new file mode 100644
index 000000000..8a510e1df
--- /dev/null
+++ b/src/mumble/PositionalData.cpp
@@ -0,0 +1,242 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PositionalData.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <stdexcept>
+
+#include <QtCore/QReadLocker>
+
+Vector3D::Vector3D() : x(0.0f), y(0.0f), z(0.0f) {
+}
+
+Vector3D::Vector3D(float x, float y, float z) : x(x), y(y), z(z) {
+}
+
+Vector3D::Vector3D(const Vector3D& other) : x(other.x), y(other.y), z(other.z) {
+}
+
+Vector3D::~Vector3D() {
+}
+
+float Vector3D::operator[](Coord coord) const {
+ switch(coord) {
+ case Coord::X:
+ return x;
+ case Coord::Y:
+ return y;
+ case Coord::Z:
+ return z;
+ }
+
+ // invalid index
+ throw std::out_of_range("May only access x, y or z");
+}
+
+Vector3D Vector3D::operator*(float factor) const {
+ return { x * factor, y * factor, z * factor };
+}
+
+Vector3D Vector3D::operator/(float divisor) const {
+ return { x / divisor, y / divisor, z / divisor };
+}
+
+void Vector3D::operator*=(float factor) {
+ x *= factor;
+ y *= factor;
+ z *= factor;
+}
+
+void Vector3D::operator/=(float divisor) {
+ x /= divisor;
+ y /= divisor;
+ z /= divisor;
+}
+
+bool Vector3D::operator==(const Vector3D& other) const {
+ return equals(other, 0.0f);
+}
+
+Vector3D Vector3D::operator-(const Vector3D& other) const {
+ return { x - other.x, y - other.y, z - other.z };
+}
+
+Vector3D Vector3D::operator+(const Vector3D& other) const {
+ return { x + other.x, y + other.y, z + other.z };
+}
+
+float Vector3D::normSquared() const {
+ return x * x + y * y + z * z;
+}
+
+float Vector3D::norm() const {
+ return std::sqrt(normSquared());
+}
+
+float Vector3D::dotProduct(const Vector3D& other) const {
+ return x * other.x + y * other.y + z * other.z;
+}
+
+Vector3D Vector3D::crossProduct(const Vector3D& other) const {
+ return { y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x };
+}
+
+bool Vector3D::equals(const Vector3D& other, float threshold) const {
+ if (threshold == 0.0f) {
+ return x == other.x && y == other.y && z == other.z;
+ } else {
+ threshold = std::abs(threshold);
+
+ return std::abs(x - other.x) < threshold && std::abs(y - other.y) < threshold && std::abs(z - other.z) < threshold;
+ }
+}
+
+bool Vector3D::isZero(float threshold) const {
+ if (threshold == 0.0f) {
+ return x == 0.0f && y == 0.0f && z == 0.0f;
+ } else {
+ return std::abs(x) < threshold && std::abs(y) < threshold && std::abs(z) < threshold;
+ }
+}
+
+void Vector3D::normalize() {
+ float len = norm();
+
+ x /= len;
+ y /= len;
+ z /= len;
+}
+
+void Vector3D::toZero() {
+ x = 0.0f;
+ y = 0.0f;
+ z = 0.0f;
+}
+
+PositionalData::PositionalData()
+ : m_playerPos(),
+ m_playerDir(),
+ m_playerAxis(),
+ m_cameraPos(),
+ m_cameraDir(),
+ m_cameraAxis(),
+ m_context(),
+ m_identity(),
+ m_lock(QReadWriteLock::NonRecursive) {
+}
+
+PositionalData::PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos,
+ Vector3D cameraDir, Vector3D cameraAxis, QString context, QString identity)
+ : m_playerPos(playerPos),
+ m_playerDir(playerDir),
+ m_playerAxis(playerAxis),
+ m_cameraPos(cameraPos),
+ m_cameraDir(cameraDir),
+ m_cameraAxis(cameraAxis),
+ m_context(context),
+ m_identity(identity),
+ m_lock(QReadWriteLock::NonRecursive) {
+}
+
+PositionalData::~PositionalData() {
+}
+
+
+void PositionalData::getPlayerPos(Position3D& pos) const {
+ QReadLocker lock(&m_lock);
+
+ pos = m_playerPos;
+}
+
+Position3D PositionalData::getPlayerPos() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerPos;
+}
+
+void PositionalData::getPlayerDir(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_playerDir;
+}
+
+Vector3D PositionalData::getPlayerDir() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerDir;
+}
+
+void PositionalData::getPlayerAxis(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_playerAxis;
+}
+
+Vector3D PositionalData::getPlayerAxis() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerAxis;
+}
+
+void PositionalData::getCameraPos(Position3D& pos) const {
+ QReadLocker lock(&m_lock);
+
+ pos = m_cameraPos;
+}
+
+Position3D PositionalData::getCameraPos() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraPos;
+}
+
+void PositionalData::getCameraDir(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_cameraDir;
+}
+
+Vector3D PositionalData::getCameraDir() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraDir;
+}
+
+void PositionalData::getCameraAxis(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_cameraAxis;
+}
+
+Vector3D PositionalData::getCameraAxis() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraAxis;
+}
+
+QString PositionalData::getPlayerIdentity() const {
+ QReadLocker lock(&m_lock);
+
+ return m_identity;
+}
+
+QString PositionalData::getContext() const {
+ QReadLocker lock(&m_lock);
+
+ return m_context;
+}
+
+void PositionalData::reset() {
+ m_playerPos.toZero();
+ m_playerDir.toZero();
+ m_playerAxis.toZero();
+ m_cameraPos.toZero();
+ m_cameraDir.toZero();
+ m_cameraAxis.toZero();
+ m_context = QString();
+ m_identity = QString();
+}
diff --git a/src/mumble/PositionalData.h b/src/mumble/PositionalData.h
new file mode 100644
index 000000000..a0f6f4013
--- /dev/null
+++ b/src/mumble/PositionalData.h
@@ -0,0 +1,171 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+
+#include <QtCore/QString>
+#include <QtCore/QReadWriteLock>
+
+/// An enum for the three cartesian coordinate axes x, y and z
+enum class Coord {X=0,Y,Z};
+
+/// A 3D vector class holding an x-, y- and z-coordinate
+struct Vector3D {
+ /// The vector's x-coordinate
+ float x;
+ /// The vector's y-coordinate
+ float y;
+ /// The vector's z-coordinate
+ float z;
+
+ /// Access the respective coordinate in an array-like fashion
+ ///
+ /// @param coord The Coord to access
+ /// @returns The value of the respective coordinate
+ float operator[](Coord coord) const;
+ /// @param factor The factor to scale by
+ /// @returns A new vector that has been created by scaling this vector by the given factor
+ Vector3D operator*(float factor) const;
+ /// @param divisor The divisor to apply to all coordinates
+ /// @returns A new vector obtained from this one by applying the divisor to all coordinates
+ Vector3D operator/(float divisor) const;
+ /// Scales this vector by the given factor
+ ///
+ /// @param factor The factor to use
+ void operator*=(float factor);
+ /// Divides all of this vector's coordinates by the given divisor
+ ///
+ /// @param divisor The divisor to use
+ void operator/=(float divisor);
+ /// @param other The vector to compare this one to
+ /// @returns Whether the given vector is equal to this one (their coordinates are the same)
+ bool operator==(const Vector3D& other) const;
+ /// @param other The vector to subtract from this one
+ /// @returns A new vector representing the difference of this vector and the other one
+ Vector3D operator-(const Vector3D& other) const;
+ /// @param other The vector to add to this one
+ /// @returns A new vector representing the sum of this vector and the other one
+ Vector3D operator+(const Vector3D& other) const;
+ /// @param other The vector to copy
+ /// @returns A copy of the other vector
+ Vector3D& operator=(const Vector3D& other) = default;
+
+ // allow explicit conversions from this struct to a float-array / float-pointer
+ /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector
+ explicit operator const float*() const { return &x; };
+ /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector
+ explicit operator float*() { return &x; };
+
+ /// Default constructor - sets all coordinates to 0
+ Vector3D();
+ /// @param x The x-coordinate
+ /// @param y The y-coordinate
+ /// @param z The z-coordinate
+ Vector3D(float x, float y, float z);
+ /// Copy constructor
+ ///
+ /// @param other The vector to copy
+ Vector3D(const Vector3D& other);
+ /// Destructor
+ ~Vector3D();
+ /// @returns The squared euclidean norm (length of the vector)
+ float normSquared() const;
+ /// If possible normSquared() should be preferred as this doesn't require a square-root operator
+ ///
+ /// @returns The euclidean norm (length of the vector)
+ float norm() const;
+ /// @param other The vector to calculate the dot-product with
+ /// @returns The dot-product between this vector an the other one
+ float dotProduct(const Vector3D& other) const;
+ /// @param other The vector to calculate the cross-product (vector-product) with
+ /// @returns The vector resulting from the cross-product (vector-product)
+ Vector3D crossProduct(const Vector3D& other) const;
+ /// @param other The vector to compare this one to
+ /// @param threshold The maximum absolute difference for coordinates to still be considered equal
+ /// @returns Whether this and the given vector are equal
+ bool equals(const Vector3D& other, float threshold = 0.0f) const;
+ /// @param threshold The maximum absolute value a coordinate may have to still be considered zero
+ /// @returns Whether this vector is the zero-vector
+ bool isZero(float threshold = 0.0f) const;
+ /// Normalizes this vector to a unit-vector. Callin this function on a zero-vector results in undefined behaviour!
+ void normalize();
+ /// Transforms this vector to a zero-vector by setting all coordinates to zero
+ void toZero();
+};
+
+// As we're casting the vector struct to float-arrays, we have to make sure that the compiler won't introduce any padding
+// into the structure
+static_assert(sizeof(Vector3D) == 3*sizeof(float), "The compiler added padding to the Vector3D structure so it can't be cast to a float-array!");
+
+/// A convenient alias as a position can be treated the same way a vector can
+typedef Vector3D Position3D;
+
+
+/// A class holding positional data used in the positional audio feature
+class PositionalData {
+ friend class PluginManager; // needed in order for PluginManager::fetch to write to the contained fields
+ protected:
+ /// The player's position in the 3D world
+ Position3D m_playerPos;
+ /// The direction in which the player is looking
+ Vector3D m_playerDir;
+ /// The connection vector between the player's feet and his/her head
+ Vector3D m_playerAxis;
+ /// The camera's position un the 3D world
+ Position3D m_cameraPos;
+ /// The direction in which the camera is looking
+ Vector3D m_cameraDir;
+ /// The connection from the camera's bottom to its top
+ Vector3D m_cameraAxis;
+ /// The context of this positional data. This might include the game's name, the server currently connected to, etc. and is used
+ /// to determine which players can hear one another
+ QString m_context;
+ /// The player's ingame identity (name)
+ QString m_identity;
+ /// The lock guarding all fields of this class
+ mutable QReadWriteLock m_lock;
+
+ public:
+ /// Default constructor
+ PositionalData();
+ /// Constructor initializing all fields to a specific value
+ PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos, Vector3D cameraDir,
+ Vector3D cameraAxis, QString context, QString identity);
+ /// Destructor
+ ~PositionalData();
+ /// @param[out] pos The player's 3D position
+ void getPlayerPos(Position3D& pos) const;
+ /// @returns The player's 3D position
+ Position3D getPlayerPos() const;
+ /// @param[out] vec The direction in which the player is currently looking
+ void getPlayerDir(Vector3D& vec) const;
+ /// @returns The direction in which the player is currently looking
+ Vector3D getPlayerDir() const;
+ /// @param[out] axis The connection between the player's feet and his/her head
+ void getPlayerAxis(Vector3D& axis) const;
+ /// @returns The connection between the player's feet and his/her head
+ Vector3D getPlayerAxis() const;
+ /// @param[out] pos The camera's 3D position
+ void getCameraPos(Position3D& pos) const;
+ /// @returns The camera's 3D position
+ Position3D getCameraPos() const;
+ /// @param[out] vec The direction in which the camera is currently looking
+ void getCameraDir(Vector3D& vec) const;
+ /// @returns The direction in which the camera is currently looking
+ Vector3D getCameraDir() const;
+ /// @param[out] axis The connection between the player's feet and his/her head
+ void getCameraAxis(Vector3D& axis) const;
+ /// @returns The connection between the player's feet and his/her head
+ Vector3D getCameraAxis() const;
+ /// @returns The player's identity
+ QString getPlayerIdentity() const;
+ /// @returns The current context
+ QString getContext() const;
+ /// Resets all fields in this object
+ void reset();
+};
+
+#endif
diff --git a/src/mumble/ServerHandler.cpp b/src/mumble/ServerHandler.cpp
index 1e7291aac..429adc3f3 100644
--- a/src/mumble/ServerHandler.cpp
+++ b/src/mumble/ServerHandler.cpp
@@ -57,6 +57,10 @@
# include <sys/socket.h>
#endif
+// Init ServerHandler::nextConnectionID
+int ServerHandler::nextConnectionID = -1;
+QMutex ServerHandler::nextConnectionIDMutex(QMutex::Recursive);
+
ServerHandlerMessageEvent::ServerHandlerMessageEvent(const QByteArray &msg, unsigned int mtype, bool flush)
: QEvent(static_cast< QEvent::Type >(SERVERSEND_EVENT)) {
qbaMsg = msg;
@@ -109,6 +113,13 @@ ServerHandler::ServerHandler() : database(new Database(QLatin1String("ServerHand
uiVersion = 0;
iInFlightTCPPings = 0;
+ // assign connection ID
+ {
+ QMutexLocker lock(&nextConnectionIDMutex);
+ nextConnectionID++;
+ connectionID = nextConnectionID;
+ }
+
// Historically, the qWarning line below initialized OpenSSL for us.
// It used to have this comment:
//
@@ -177,6 +188,10 @@ void ServerHandler::customEvent(QEvent *evt) {
}
}
+int ServerHandler::getConnectionID() const {
+ return connectionID;
+}
+
void ServerHandler::udpReady() {
const unsigned int UDP_MAX_SIZE = 2048;
while (qusUdp->hasPendingDatagrams()) {
@@ -683,6 +698,9 @@ void ServerHandler::serverConnectionClosed(QAbstractSocket::SocketError err, con
}
}
+ // Having 2 signals here that basically fire at the same time is wanted behavior!
+ // See the documentation of "aboutToDisconnect" for an explanation.
+ emit aboutToDisconnect(err, reason);
emit disconnected(err, reason);
exit(0);
diff --git a/src/mumble/ServerHandler.h b/src/mumble/ServerHandler.h
index 5fc29c504..d20833aff 100644
--- a/src/mumble/ServerHandler.h
+++ b/src/mumble/ServerHandler.h
@@ -62,6 +62,9 @@ private:
Database *database;
+ static QMutex nextConnectionIDMutex;
+ static int nextConnectionID;
+
protected:
QString qsHostName;
QString qsUserName;
@@ -70,6 +73,7 @@ protected:
unsigned short usResolvedPort;
bool bUdp;
bool bStrong;
+ int connectionID;
/// Flag indicating whether the server we are currently connected to has
/// finished synchronizing already.
@@ -117,6 +121,7 @@ public:
void getConnectionInfo(QString &host, unsigned short &port, QString &username, QString &pw) const;
bool isStrong() const;
void customEvent(QEvent *evt) Q_DECL_OVERRIDE;
+ int getConnectionID() const;
void sendProtoMessage(const ::google::protobuf::Message &msg, unsigned int msgType);
void sendMessage(const char *data, int len, bool force = false);
@@ -169,6 +174,11 @@ public:
void run() Q_DECL_OVERRIDE;
signals:
void error(QAbstractSocket::SocketError, QString reason);
+ // This signal is basically the same as disconnected but it will be emitted
+ // *right before* disconnected is emitted. Thus this can be used by slots
+ // that need to block the disconnected signal from being emitted (using a
+ // direct connection) before they're done.
+ void aboutToDisconnect(QAbstractSocket::SocketError, QString reason);
void disconnected(QAbstractSocket::SocketError, QString reason);
void connected();
void pingRequested();
diff --git a/src/mumble/Settings.cpp b/src/mumble/Settings.cpp
index 31d91bc68..aac13c4a9 100644
--- a/src/mumble/Settings.cpp
+++ b/src/mumble/Settings.cpp
@@ -15,6 +15,8 @@
#include <QtCore/QProcessEnvironment>
#include <QtCore/QStandardPaths>
+#include <QtCore/QFileInfo>
+#include <QtCore/QRegularExpression>
#include <QtGui/QImageReader>
#include <QtWidgets/QSystemTrayIcon>
#if QT_VERSION >= QT_VERSION_CHECK(5,9,0)
@@ -287,6 +289,8 @@ Settings::Settings() {
qRegisterMetaType< ShortcutTarget >("ShortcutTarget");
qRegisterMetaTypeStreamOperators< ShortcutTarget >("ShortcutTarget");
qRegisterMetaType< QVariant >("QVariant");
+ qRegisterMetaType< PluginSetting >("PluginSetting");
+ qRegisterMetaTypeStreamOperators< PluginSetting >("PluginSetting");
atTransmit = VAD;
bTransmitPosition = false;
@@ -346,6 +350,7 @@ Settings::Settings() {
bUpdateCheck = true;
bPluginCheck = true;
#endif
+ bPluginAutoUpdate = false;
qsImagePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
@@ -543,7 +548,8 @@ Settings::Settings() {
qmMessages[Log::OtherSelfMute] = Settings::LogConsole;
qmMessages[Log::OtherMutedOther] = Settings::LogConsole;
qmMessages[Log::UserRenamed] = Settings::LogConsole;
-
+ qmMessages[Log::PluginMessage] = Settings::LogConsole;
+
// Default theme
themeName = QLatin1String("Mumble");
themeStyleName = QLatin1String("Lite");
@@ -888,6 +894,7 @@ void Settings::load(QSettings *settings_ptr) {
LOAD(bUpdateCheck, "ui/updatecheck");
LOAD(bPluginCheck, "ui/plugincheck");
+ LOAD(bPluginAutoUpdate, "ui/pluginAutoUpdate");
LOAD(bHideInTray, "ui/hidetray");
LOAD(bStateInTray, "ui/stateintray");
@@ -1002,9 +1009,26 @@ void Settings::load(QSettings *settings_ptr) {
}
settings_ptr->endGroup();
- settings_ptr->beginGroup(QLatin1String("audio/plugins"));
- foreach (const QString &d, settings_ptr->childKeys()) {
- qmPositionalAudioPlugins.insert(d, settings_ptr->value(d, true).toBool());
+ // Plugins
+ settings_ptr->beginGroup(QLatin1String("plugins"));
+ foreach(const QString &pluginKey, settings_ptr->childGroups()) {
+ QString pluginHash;
+
+ if (pluginKey.contains(QLatin1String("_"))) {
+ // The key contains the filename as well as the hash
+ int index = pluginKey.lastIndexOf(QLatin1String("_"));
+ pluginHash = pluginKey.right(pluginKey.size() - index - 1);
+ } else {
+ pluginHash = pluginKey;
+ }
+
+ PluginSetting pluginSettings;
+ pluginSettings.path = settings_ptr->value(pluginKey + QLatin1String("/path")).toString();
+ pluginSettings.allowKeyboardMonitoring = settings_ptr->value(pluginKey + QLatin1String("/allowKeyboardMonitoring")).toBool();
+ pluginSettings.enabled = settings_ptr->value(pluginKey + QLatin1String("/enabled")).toBool();
+ pluginSettings.positionalDataEnabled = settings_ptr->value(pluginKey + QLatin1String("/positionalDataEnabled")).toBool();
+
+ qhPluginSettings.insert(pluginHash, pluginSettings);
}
settings_ptr->endGroup();
@@ -1259,8 +1283,12 @@ void Settings::save() {
SAVE(qsUsername, "ui/username");
SAVE(qsLastServer, "ui/server");
SAVE(ssFilter, "ui/serverfilter");
+#ifndef NO_UPDATE_CHECK
+ // If this flag has been set, we don't load the following settings so we shouldn't overwrite them here either
SAVE(bUpdateCheck, "ui/updatecheck");
SAVE(bPluginCheck, "ui/plugincheck");
+ SAVE(bPluginAutoUpdate, "ui/pluginAutoUpdate");
+#endif
SAVE(bHideInTray, "ui/hidetray");
SAVE(bStateInTray, "ui/stateintray");
SAVE(bUsage, "ui/usage");
@@ -1373,22 +1401,56 @@ void Settings::save() {
settings_ptr->remove(d);
}
settings_ptr->endGroup();
+
+ // Plugins
+ foreach(const QString &pluginHash, qhPluginSettings.keys()) {
+ QString savePath = QString::fromLatin1("plugins/");
+ const PluginSetting settings = qhPluginSettings.value(pluginHash);
+ const QFileInfo info(settings.path);
+ QString baseName = info.baseName(); // Get the filename without file extensions
+ const bool containsNonASCII = baseName.contains(QRegularExpression(QStringLiteral("[^\\x{0000}-\\x{007F}]")));
+
+ if (containsNonASCII || baseName.isEmpty()) {
+ savePath += pluginHash;
+ } else {
+ // Make sure there are no spaces in the name
+ baseName.replace(QLatin1Char(' '), QLatin1Char('_'));
+
+ // Also include the plugin's filename in the savepath in order
+ // to allow for easier identification
+ savePath += baseName + QLatin1String("__") + pluginHash;
+ }
- settings_ptr->beginGroup(QLatin1String("audio/plugins"));
- foreach (const QString &d, qmPositionalAudioPlugins.keys()) {
- bool v = qmPositionalAudioPlugins.value(d);
- if (!v)
- settings_ptr->setValue(d, v);
- else
- settings_ptr->remove(d);
+ settings_ptr->beginGroup(savePath);
+ settings_ptr->setValue(QLatin1String("path"), settings.path);
+ settings_ptr->setValue(QLatin1String("enabled"), settings.enabled);
+ settings_ptr->setValue(QLatin1String("positionalDataEnabled"), settings.positionalDataEnabled);
+ settings_ptr->setValue(QLatin1String("allowKeyboardMonitoring"), settings.allowKeyboardMonitoring);
+ settings_ptr->endGroup();
}
- settings_ptr->endGroup();
+
settings_ptr->beginGroup(QLatin1String("overlay"));
os.save(settings_ptr);
settings_ptr->endGroup();
}
+QDataStream& operator>>(QDataStream &arch, PluginSetting &setting) {
+ arch >> setting.enabled;
+ arch >> setting.positionalDataEnabled;
+ arch >> setting.allowKeyboardMonitoring;
+
+ return arch;
+}
+
+QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting) {
+ arch << setting.enabled;
+ arch << setting.positionalDataEnabled;
+ arch << setting.allowKeyboardMonitoring;
+
+ return arch;
+}
+
#undef LOAD
#undef LOADENUM
#undef LOADFLAG
diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h
index a074dd6ad..ab46e5391 100644
--- a/src/mumble/Settings.h
+++ b/src/mumble/Settings.h
@@ -59,6 +59,17 @@ QDataStream &operator<<(QDataStream &, const ShortcutTarget &);
QDataStream &operator>>(QDataStream &, ShortcutTarget &);
Q_DECLARE_METATYPE(ShortcutTarget)
+struct PluginSetting {
+ QString path;
+ bool enabled;
+ bool positionalDataEnabled;
+ bool allowKeyboardMonitoring;
+};
+QDataStream& operator>>(QDataStream &arch, PluginSetting &setting);
+QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting);
+Q_DECLARE_METATYPE(PluginSetting);
+
+
struct OverlaySettings {
enum OverlayPresets { AvatarAndName, LargeSquareAvatar };
@@ -249,7 +260,9 @@ struct Settings {
bool bPositionalAudio;
bool bPositionalHeadphone;
float fAudioMinDistance, fAudioMaxDistance, fAudioMaxDistVolume, fAudioBloom;
- QMap< QString, bool > qmPositionalAudioPlugins;
+ /// Contains the settings for each individual plugin. The key in this map is the Hex-represented SHA-1
+ /// hash of the plugin's UTF-8 encoded absolute file-path on the hard-drive.
+ QHash< QString, PluginSetting > qhPluginSettings;
OverlaySettings os;
@@ -351,6 +364,7 @@ struct Settings {
bool bUpdateCheck;
bool bPluginCheck;
+ bool bPluginAutoUpdate;
// PTT Button window
bool bShowPTTButtonWindow;
diff --git a/src/mumble/UserModel.cpp b/src/mumble/UserModel.cpp
index 35fca925d..ceb7c1dd4 100644
--- a/src/mumble/UserModel.cpp
+++ b/src/mumble/UserModel.cpp
@@ -1052,6 +1052,8 @@ ClientUser *UserModel::addUser(unsigned int id, const QString &name) {
updateOverlay();
+ emit userAdded(p->uiSession);
+
return p;
}
@@ -1087,6 +1089,8 @@ void UserModel::removeUser(ClientUser *p) {
updateOverlay();
+ emit userRemoved(p->uiSession);
+
delete p;
delete item;
}
@@ -1303,6 +1307,8 @@ void UserModel::renameChannel(Channel *c, const QString &name) {
moveItem(pi, pi, item);
}
+
+ emit channelRenamed(c->iId);
}
void UserModel::repositionChannel(Channel *c, const int position) {
@@ -1341,6 +1347,9 @@ Channel *UserModel::addChannel(int id, Channel *p, const QString &name) {
if (Global::get().s.ceExpand == Settings::AllChannels)
Global::get().mw->qtvUsers->setExpanded(index(item), true);
+
+ emit channelAdded(c->iId);
+
return c;
}
@@ -1501,6 +1510,8 @@ bool UserModel::removeChannel(Channel *c, const bool onlyIfUnoccupied) {
Channel::remove(c);
+ emit channelRemoved(c->iId);
+
delete item;
delete c;
return true;
diff --git a/src/mumble/UserModel.h b/src/mumble/UserModel.h
index c9824f25a..330500509 100644
--- a/src/mumble/UserModel.h
+++ b/src/mumble/UserModel.h
@@ -213,6 +213,27 @@ public slots:
void recheckLinks();
void updateOverlay() const;
void toggleChannelFiltered(Channel *c);
+signals:
+ /// A signal emitted whenever a user is added to the model.
+ ///
+ /// @param userSessionID The ID of that user's session
+ void userAdded(unsigned int userSessionID);
+ /// A signal emitted whenever a user is removed from the model.
+ ///
+ /// @param userSessionID The ID of that user's session
+ void userRemoved(unsigned int userSessionID);
+ /// A signal that emitted whenever a channel is added to the model.
+ ///
+ /// @param channelID The ID of the channel
+ void channelAdded(int channelID);
+ /// A signal that emitted whenever a channel is removed from the model.
+ ///
+ /// @param channelID The ID of the channel
+ void channelRemoved(int channelID);
+ /// A signal that emitted whenever a channel is renamed.
+ ///
+ /// @param channelID The ID of the channel
+ void channelRenamed(int channelID);
};
#endif
diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp
index dad3cce16..a1ef34826 100644
--- a/src/mumble/main.cpp
+++ b/src/mumble/main.cpp
@@ -11,12 +11,13 @@
#include "AudioWizard.h"
#include "Cert.h"
#include "Database.h"
+#include "Log.h"
+#include "LogEmitter.h"
#include "DeveloperConsole.h"
#include "LCD.h"
#include "Log.h"
#include "LogEmitter.h"
#include "MainWindow.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#ifdef USE_ZEROCONF
# include "Zeroconf.h"
@@ -43,6 +44,9 @@
#include "Themes.h"
#include "UserLockFile.h"
#include "VersionCheck.h"
+#include "PluginInstaller.h"
+#include "PluginManager.h"
+#include "Global.h"
#include <QtCore/QProcess>
#include <QtGui/QDesktopServices>
@@ -58,7 +62,6 @@
# include <shellapi.h>
#endif
-#include "Global.h"
#ifdef BOOST_NO_EXCEPTIONS
namespace boost {
@@ -229,6 +232,7 @@ int main(int argc, char **argv) {
QStringList extraTranslationDirs;
QString localeOverwrite;
+ QStringList pluginsToBeInstalled;
if (a.arguments().count() > 1) {
for (int i = 1; i < args.count(); ++i) {
if (args.at(i) == QLatin1String("-h") || args.at(i) == QLatin1String("--help")
@@ -237,13 +241,15 @@ int main(int argc, char **argv) {
#endif
) {
QString helpMessage =
- MainWindow::tr("Usage: mumble [options] [<url>]\n"
+ MainWindow::tr("Usage: mumble [options] [<url> | <plugin_list>]\n"
"\n"
"<url> specifies a URL to connect to after startup instead of showing\n"
"the connection window, and has the following form:\n"
"mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/"
"<subchannel>...]][?version=<x.y.z>]\n"
"\n"
+ "<plugin_list> is a list of plugin files that shall be installed"
+ "\n"
"The version query parameter has to be set in order to invoke the\n"
"correct client version. It currently defaults to 1.2.0.\n"
"\n"
@@ -399,14 +405,18 @@ int main(int argc, char **argv) {
return 1;
}
} else {
- if (!bRpcMode) {
- QUrl u = QUrl::fromEncoded(args.at(i).toUtf8());
- if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
- url = u;
- } else {
- QFile f(args.at(i));
- if (f.exists()) {
- url = QUrl::fromLocalFile(f.fileName());
+ if (PluginInstaller::canBePluginFile(args.at(i))) {
+ pluginsToBeInstalled << args.at(i);
+ } else {
+ if (!bRpcMode) {
+ QUrl u = QUrl::fromEncoded(args.at(i).toUtf8());
+ if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
+ url = u;
+ } else {
+ QFile f(args.at(i));
+ if (f.exists()) {
+ url = QUrl::fromLocalFile(f.fileName());
+ }
}
}
}
@@ -583,6 +593,22 @@ int main(int argc, char **argv) {
Global::get().s.qsLanguage = settingsLocale.nativeLanguageName();
}
+ if (!pluginsToBeInstalled.isEmpty()) {
+
+ foreach(QString currentPlugin, pluginsToBeInstalled) {
+
+ try {
+ PluginInstaller installer(currentPlugin);
+ installer.exec();
+ } catch(const PluginInstallException& e) {
+ qCritical() << qUtf8Printable(e.getMessage());
+ }
+
+ }
+
+ return 0;
+ }
+
qWarning("Locale is \"%s\" (System: \"%s\")", qUtf8Printable(settingsLocale.name()), qUtf8Printable(systemLocale.name()));
Mumble::Translations::LifetimeGuard translationGuard = Mumble::Translations::installTranslators(settingsLocale, a, extraTranslationDirs);
@@ -605,6 +631,10 @@ int main(int argc, char **argv) {
// Initialize zeroconf
Global::get().zeroconf = new Zeroconf();
#endif
+
+ // PluginManager
+ Global::get().pluginManager = new PluginManager();
+ Global::get().pluginManager->rescanPlugins();
#ifdef USE_OVERLAY
Global::get().o = new Overlay();
@@ -661,10 +691,6 @@ int main(int argc, char **argv) {
Global::get().l->log(Log::Information, MainWindow::tr("Welcome to Mumble."));
- // Plugins
- Global::get().p = new Plugins(nullptr);
- Global::get().p->rescanPlugins();
-
Audio::start();
a.setQuitOnLastWindowClosed(false);
@@ -736,12 +762,13 @@ int main(int argc, char **argv) {
new VersionCheck(false, Global::get().mw, true);
# endif
}
-#else
- Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode."));
-#endif
+
if (Global::get().s.bPluginCheck) {
- Global::get().p->checkUpdates();
+ Global::get().pluginManager->checkForPluginUpdates();
}
+#else // QT_NO_DEBUG
+ Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode."));
+#endif // QT_NO_DEBUG
if (url.isValid()) {
OpenURLEvent *oue = new OpenURLEvent(url);
@@ -772,8 +799,20 @@ int main(int argc, char **argv) {
// Wait for the ServerHandler thread to exit before proceeding shutting down. This is so that
// all events that the ServerHandler might emit are enqueued into Qt's event loop before we
// ask it to pocess all of them below.
- if (!sh->wait(2000)) {
- qCritical("main: ServerHandler did not exit within specified time interval");
+
+ // We iteratively probe whether the ServerHandler thread has finished yet. If it did
+ // not, we execute pending events in the main loop. This is because the ServerHandler
+ // could be stuck waiting for a function to complete in the main loop (e.g. a plugin
+ // uses the API in the disconnect callback).
+ // We assume that this entire process is done in way under a second.
+ int iterations = 0;
+ while (!sh->wait(10)) {
+ QCoreApplication::processEvents();
+ iterations++;
+
+ if (iterations > 200) {
+ qFatal("ServerHandler does not exit as expected");
+ }
}
}
@@ -785,20 +824,26 @@ int main(int argc, char **argv) {
delete srpc;
+ delete Global::get().talkingUI;
+ // Delete the MainWindow before the ServerHandler gets reset in order to allow all callbacks
+ // trggered by this deletion to still access the ServerHandler (atm all these callbacks are in PluginManager.cpp)
+ delete Global::get().mw;
+ Global::get().mw = nullptr; // Make it clear to any destruction code, that MainWindow no longer exists
+
Global::get().sh.reset();
- while (sh && !sh.unique())
+
+ while (sh && ! sh.unique())
QThread::yieldCurrentThread();
sh.reset();
- delete Global::get().talkingUI;
- delete Global::get().mw;
-
delete Global::get().nam;
delete Global::get().lcd;
delete Global::get().db;
- delete Global::get().p;
delete Global::get().l;
+ Global::get().l = nullptr; // Make it clear to any destruction code that Log no longer exists
+
+ delete Global::get().pluginManager;
#ifdef USE_ZEROCONF
delete Global::get().zeroconf;
diff --git a/src/mumble/mumble_ar.ts b/src/mumble/mumble_ar.ts
index 71cf51bd8..4fc00bb4e 100644
--- a/src/mumble/mumble_ar.ts
+++ b/src/mumble/mumble_ar.ts
@@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>المستخدم توقف عن الاستماع الى القناة</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">اسم</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_bg.ts b/src/mumble/mumble_bg.ts
index 4f3c1690e..8ff93bd2e 100644
--- a/src/mumble/mumble_bg.ts
+++ b/src/mumble/mumble_bg.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation>Свързване към последния сървър при пускане на програмата</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Сваляне на обновления за слоя и приставките при пускане на програмата</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Поверителност</translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Име</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Включено</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_br.ts b/src/mumble/mumble_br.ts
index 5fed8a97e..56c3e4cd4 100644
--- a/src/mumble/mumble_br.ts
+++ b/src/mumble/mumble_br.ts
@@ -3778,6 +3778,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6121,12 +6125,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6446,10 +6451,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6474,6 +6475,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7024,30 +7033,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Anv</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_ca.ts b/src/mumble/mumble_ca.ts
index d545cbe6a..df853080e 100644
--- a/src/mumble/mumble_ca.ts
+++ b/src/mumble/mumble_ca.ts
@@ -3784,6 +3784,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6127,12 +6131,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6452,10 +6457,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6480,6 +6481,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7030,30 +7039,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nom</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_cs.ts b/src/mumble/mumble_cs.ts
index df4b57b60..d564ee872 100644
--- a/src/mumble/mumble_cs.ts
+++ b/src/mumble/mumble_cs.ts
@@ -3833,6 +3833,10 @@ Toto pole popisuje velikost LCD zařízení. Velikost je udávána buď v pixele
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6181,12 +6185,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6506,10 +6511,6 @@ Valid options are:
<translation>Při startu se znovu připojit na poslední server</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Při startu stáhnout aktualizace zásuvných modulů a překryvů</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6534,6 +6535,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7088,31 +7097,189 @@ Pro aktualizaci těchto souborů na jejich poslední verzi, klikněte na tlačí
<translation>Jméno</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Povoleno</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>V režimu ladění přeskakuji aktualizaci zásuvných modulů.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Aktualizován nebo stáhnut nový zásuvný modul do %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nelze instalovat nový zásuvný modul do %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 ztraceno propojení.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 propojen.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_cy.ts b/src/mumble/mumble_cy.ts
index 19d907b49..d3ce85dca 100644
--- a/src/mumble/mumble_cy.ts
+++ b/src/mumble/mumble_cy.ts
@@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation>Ailgysylltu i&apos;r gweinydd olaf ar gychwyn</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Enw</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Wedi&apos;i alluogi</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_da.ts b/src/mumble/mumble_da.ts
index c480f2470..72d08e75f 100644
--- a/src/mumble/mumble_da.ts
+++ b/src/mumble/mumble_da.ts
@@ -3831,6 +3831,10 @@ Dette felt beskriver størrelsen af en LCD-enhed. Størrelsen er enten opgivet i
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6177,12 +6181,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6502,10 +6507,6 @@ Valid options are:
<translation>Forbind igen til sidst anvendte server ved opstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Download plugin- og overlægningsopdateringer ved opstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6530,6 +6531,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7084,31 +7093,189 @@ For at opgradere disse filer til deres nyeste version, klik på knappen nedenfor
<translation>Navn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiveret</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Springer over pluginopdateringer i fejlfindingstilstand.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Downloadede nyt eller opdateret plugin til %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installation af nyt plugin til %1 mislykkedes.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 mistede link.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 er linket.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_de.ts b/src/mumble/mumble_de.ts
index 5ac64fbe7..3ceee3768 100644
--- a/src/mumble/mumble_de.ts
+++ b/src/mumble/mumble_de.ts
@@ -3882,6 +3882,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>Benutzer hört dem Kanal nicht mehr zu</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6239,12 +6243,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6567,10 +6572,6 @@ Mumble hat ein kleines Entwickler-Team. Deshalb muss die verfügbare Zeit auf di
<translation>Beim Start zum zuletzt benutzten Server verbinden</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Plugin und Overlay Updates beim Starten herunterladen</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Datenschutz</translation>
</message>
@@ -6596,6 +6597,14 @@ Verhindert, dass potenziell identifizierende Informationen über das Betriebssys
<source>Hide public server list</source>
<translation>Öffentliche Serverliste verstecken</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7151,31 +7160,189 @@ Um diese Dateien zu aktualisieren, klicken Sie unten den Button.</translation>
<translation>Name</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiviert</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Aktivieren</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Überspringe Plugin-Aktualisierung im Debug-Modus.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Neues oder aktualisiertes Plugin nach %1 heruntergeladen.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installation eines neuen Plugins nach %1 fehlgeschlagen.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 hat Verbindung verloren.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 verbunden.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_el.ts b/src/mumble/mumble_el.ts
index a577a1e36..761c641a3 100644
--- a/src/mumble/mumble_el.ts
+++ b/src/mumble/mumble_el.ts
@@ -3835,6 +3835,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6186,12 +6190,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6511,10 +6516,6 @@ Valid options are:
<translation>Επανασύνδεση με τον τελευταίο διακομιστή που χρησιμοποιήθηκε κατά την εκκίνηση.</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Να γίνεται λήψη ενημερώσεων για τα πρόσθετα και το overlay κατά την εκκίνηση</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Ιδιωτικότητα</translation>
</message>
@@ -6540,6 +6541,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7094,31 +7103,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Όνομα</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ενεργοποιημένο</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Παράλειψη της ενημέρωσης των πρόσθετων στη λειτουργία εντοπισμού σφαλμάτων.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Έγινε λήψη νέου ή ενημερωμένου πρόσθετου στο %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Απέτυχε η εγκατάσταση του νέου πρόσθετου στο %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 χαμένος σύνδεσμος.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 σύνδεσμος.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_en.ts b/src/mumble/mumble_en.ts
index 4a0358e0d..49e2aceb2 100644
--- a/src/mumble/mumble_en.ts
+++ b/src/mumble/mumble_en.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"></translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_en_GB.ts b/src/mumble/mumble_en_GB.ts
index c1cd32e0d..63b1260bb 100644
--- a/src/mumble/mumble_en_GB.ts
+++ b/src/mumble/mumble_en_GB.ts
@@ -3814,6 +3814,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6157,12 +6161,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6482,10 +6487,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6510,6 +6511,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7060,30 +7069,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Name</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_eo.ts b/src/mumble/mumble_eo.ts
index 614e15add..91f0f740a 100644
--- a/src/mumble/mumble_eo.ts
+++ b/src/mumble/mumble_eo.ts
@@ -3786,6 +3786,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6130,12 +6134,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6455,10 +6460,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privateco</translation>
</message>
@@ -6483,6 +6484,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>Kaŝi la publikan servilaliston</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7033,30 +7042,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Nomo</translation>
</message>
<message>
- <source>Enabled</source>
- <translation type="unfinished">Enŝaltite</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_es.ts b/src/mumble/mumble_es.ts
index 14299e098..9bfdc9a17 100644
--- a/src/mumble/mumble_es.ts
+++ b/src/mumble/mumble_es.ts
@@ -3838,6 +3838,10 @@ Este campo describe el tamaño de un dispositivo LCD. El tamaño se da, o bien e
<source>User stopped listening to channel</source>
<translation>El usuario dejó de escuchar en su canal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6191,12 +6195,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6516,10 +6521,6 @@ Valid options are:
<translation>Reconectar al último servidor al inicio</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Descargar actualizaciones de los complementos y la superposición al inicio</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacidad</translation>
</message>
@@ -6545,6 +6546,14 @@ Impide que el cliente envíe información potencialmente identificable sobre el
<source>Hide public server list</source>
<translation>Esconder la lista de servidores públicos</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7099,31 +7108,189 @@ Para actualizar estos ficheros a la última versión, haga clic en el botón inf
<translation>Nombre</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Habilitado</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Habilitar</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Omitiendo la actualización de los complementos en el modo de depuración.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Se ha descargado un complemento nuevo o actualizado para %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>No se pudo instalar un nuevo complemento para %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdió el vínculo.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 vinculado.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_et.ts b/src/mumble/mumble_et.ts
index 7277cff3d..8170471b1 100644
--- a/src/mumble/mumble_et.ts
+++ b/src/mumble/mumble_et.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privaatsus</translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,31 +7034,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Nimi</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Lubatud</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Luba</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>%1 lost link</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>%1 linked</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 lingitud.</translation>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_eu.ts b/src/mumble/mumble_eu.ts
index f7038bb97..21a710694 100644
--- a/src/mumble/mumble_eu.ts
+++ b/src/mumble/mumble_eu.ts
@@ -3796,6 +3796,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6141,12 +6145,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6466,10 +6471,6 @@ Valid options are:
<translation>Birkonektatu azken zerbitzarira hastean</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6494,6 +6495,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7044,31 +7053,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Izena</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Gaituta</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>%1 lost link</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Eguneratutako edo berria den gehigarria deskargatuta %1 -era.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1-era gehigarri berria instalatzean errorea.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 lotura galdua.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 lotuta.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_fa_IR.ts b/src/mumble/mumble_fa_IR.ts
index c139066bd..1d1e9b2f4 100644
--- a/src/mumble/mumble_fa_IR.ts
+++ b/src/mumble/mumble_fa_IR.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">نام</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_fi.ts b/src/mumble/mumble_fi.ts
index 252b08ab2..8948e902c 100644
--- a/src/mumble/mumble_fi.ts
+++ b/src/mumble/mumble_fi.ts
@@ -3838,6 +3838,10 @@ Kenttä kuvaa LCD-laitteen koon. Koko annetaan joko pikseleinä (graafinen LCD)
<source>User stopped listening to channel</source>
<translation>Käyttäjä lopetti kanavan kuuntelun</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6231,12 +6235,13 @@ Päteviä valintoja ovat:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6556,10 +6561,6 @@ Valid options are:
<translation>Yhdistä viimeisimpään palvelimeen käynnistymisen yhteydessä</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Lataa liitännäisten ja overlayn päivitykset ohjelman käynnistymisen yhteydessä</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Yksityisyys</translation>
</message>
@@ -6585,6 +6586,14 @@ Estää mahdollisesti tunnistamista helpottavien tietojen, koskien käyttöjärj
<source>Hide public server list</source>
<translation>Piilota julkisten palvelinten lista</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7139,31 +7148,189 @@ Paina alapuolen napista päivittääksesi Overlayn tiedostot viimeisimpään ver
<translation>Nimi</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Käytössä</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Käytä</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Liitännäispäivitys ohitetaan debug-tilassa.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Ladattiin uusi tai päivitettiin liitännäinnen %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Uuden liitännäisen asennus epäonnistui %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 yhteys kadotettu.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 yhdistetty.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_fr.ts b/src/mumble/mumble_fr.ts
index f559e2ec4..69846001c 100644
--- a/src/mumble/mumble_fr.ts
+++ b/src/mumble/mumble_fr.ts
@@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>L&apos;utilisateur a cessé d&apos;écouter le salon</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6191,12 +6195,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6516,10 +6521,6 @@ Valid options are:
<translation>Se reconnecter au dernier serveur utilisé au démarrage</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Télécharger les mises à jour des plugins et de l&apos;overlay au démarrage</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Confidentialité</translation>
</message>
@@ -6545,6 +6546,14 @@ Empêche le client d&apos;envoyer des informations pouvant identifier le systèm
<source>Hide public server list</source>
<translation>Cacher la liste des serveurs publics</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7099,31 +7108,189 @@ Pour mettre à jour l&apos;overlay, cliquez sur le bouton ci-dessous.</translati
<translation>Nom</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Activé</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Activer</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Passe la mise à jour des plugins en mode débogage.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Téléchargé le nouveau plugin ou mis à jour vers %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Échec de l&apos;installation du nouveau plugin dans %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 est désactivé.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 est activé.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_gl.ts b/src/mumble/mumble_gl.ts
index a18a12f93..efb2ebbd0 100644
--- a/src/mumble/mumble_gl.ts
+++ b/src/mumble/mumble_gl.ts
@@ -3780,6 +3780,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6123,12 +6127,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6448,10 +6453,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6476,6 +6477,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7026,30 +7035,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nome</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_he.ts b/src/mumble/mumble_he.ts
index df3fd72d2..0e7680463 100644
--- a/src/mumble/mumble_he.ts
+++ b/src/mumble/mumble_he.ts
@@ -3829,6 +3829,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6174,12 +6178,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6499,10 +6504,6 @@ Valid options are:
<translation>התחבר שוב אל שרת אחרון בעת הפעלה</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>הורד עדכונים עבור תוספים וממשק-המשחק</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6527,6 +6528,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7081,32 +7090,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>שם</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>מאופשרת</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>מדלג על עדכון תוספים במצב ניפוי-שגיאות.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>מוריד או מעדכן תוסף עבור %1.
-</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>נכשל בהתקנת תוסף חדש ל-%1.</translation>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 איבד קישור.</translation>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 קושר.</translation>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_hu.ts b/src/mumble/mumble_hu.ts
index d92ca1cc1..0e6933174 100644
--- a/src/mumble/mumble_hu.ts
+++ b/src/mumble/mumble_hu.ts
@@ -3826,6 +3826,10 @@ Ez a mező mutatja egy LCD eszköz méretét. A méret vagy pixelben (a grafikus
<source>User stopped listening to channel</source>
<translation>User stopped listening to channel</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6171,12 +6175,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6496,10 +6501,6 @@ Valid options are:
<translation>Újracsatlakozás az utoljára használt kiszolgálóhoz indításkor</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Bővítmények és képátfedés frissítése indításkor</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Adatvédelem</translation>
</message>
@@ -6525,6 +6526,14 @@ Ez a beállítás meggátolja, hogy a Mumble érzékeny adatokat továbbítson a
<source>Hide public server list</source>
<translation>Nyilvános kiszolgálók listájának elrejtése</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Név</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Engedélyezett</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Bővítmény frissítésének mellőzése hibakeresési módban.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Új plugin letöltve vagy frissítve: %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nem sikerült új plugin telepítése: %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 elveszett kapcsolat.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 kapcsolva.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_it.ts b/src/mumble/mumble_it.ts
index 312571797..d178feb5f 100644
--- a/src/mumble/mumble_it.ts
+++ b/src/mumble/mumble_it.ts
@@ -3838,6 +3838,10 @@ Questo campo descrive la dimensione di un dispositivo LCD. La dimensione è espr
<source>User stopped listening to channel</source>
<translation>Un utente ha smesso di ascoltare il tuo canale</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6215,12 +6219,13 @@ Azioni valide:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6270,59 +6275,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Utilizzo: mumble [opzioni] [&lt;url&gt;]
-
-&lt;url&gt; specifica un URL a cui connettersi dopo l&apos;avvio invece di visualizzare la finestra di connessione e ha la seguente forma:
-mumble: // [&lt;username&gt; [: &lt;password&gt;] @] &lt;host&gt; [: &lt;port&gt;] [/ &lt;channel&gt; [/ &lt;subchannel&gt; ...]] [? version = &lt;x.y.z&gt;]
-
-Il parametro di query della versione deve essere impostato per richiamare la versione client corretta. Attualmente il valore predefinito è 1.2.0.
-
-Le opzioni valide sono:
- -h, --help Mostra questo testo di aiuto ed esce.
- -m, --multiple
- Consenti l&apos;avvio di più istanze del client.
- -c, --config
- Specificare un file di configurazione alternativo.
- Se lo usi per eseguire più istanze di Mumble contemporaneamente,
- assicurati di impostare un valore &quot;database&quot; alternativo nel file config.
- -n, --noidentity
- Sopprimi il caricamento dei file di identità (ad esempio, certificati).
- -jn, --jackname &lt;arg&gt;
- Imposta il nome del client Jack personalizzato.
- --licenza
- Mostra la licenza di Mumble.
- --autori
- Mostra gli autori di Mumble.
- - licenze di terze parti
- Mostra le licenze per il software di terze parti utilizzato da Mumble.
- --window-title-ext &lt;arg&gt;
- Imposta un&apos;estensione del titolo della finestra personalizzata.
- --dump-input-stream
- Esegui il dump dei flussi PCM in varie parti della catena di input
- (utile per scopi di debug)
- - ingresso microfono grezzo
- - rilettura dell&apos;altoparlante per la cancellazione dell&apos;eco
- - ingresso microfono elaborato
- --print-echocancel-queue
- Stampa su stdout lo stato della coda di cancellazione dell&apos;eco
- (utile per scopi di debug)
- --translation-dir &lt;dir&gt;
- Specifica una traduzione aggiuntiva fir &lt;dir&gt; in cui
- Mumble cercherà i file di traduzione che sovrascrivono
- quelli in bundle
- Le directory aggiunte in questo modo hanno una priorità maggiore delle
- posizioni predefinite utilizzate altrimenti
- --print-translation-dirs
- Stampa i percorsi in cui Mumble cercherà
- file di traduzione che sovrascrivono quelli in bundle.
- (Utile per i traduttori che testano le loro traduzioni)
- --locale &lt;locale&gt;
- Sovrascrivi le impostazioni locali nelle impostazioni di Mumble con un file
- locale che corrisponde alla stringa di locale specificata.
- Se il formato non è valido, Mumble genererà un errore.
- In caso contrario, la locale verrà salvata in modo permanente nelle
- impostazioni di Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6592,10 +6545,6 @@ Le opzioni valide sono:
<translation>All&apos;avvio connettiti all&apos;ultimo server visitato</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Aggiorna plugin e sovrapposizione all&apos;avvio</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacy</translation>
</message>
@@ -6621,6 +6570,14 @@ Previene l&apos;invio da parte del client di informazioni potenzialmente identif
<source>Hide public server list</source>
<translation>Nascondi lista server pubblici</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7175,31 +7132,189 @@ Per aggiornare questi file all&apos;ultima versione, premi il pulsante sottostan
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Abilitato</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Abilita</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>I plugin non verranno aggiornati in modalità Debug.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Aggiunto o aggiornato il seguente plugin: %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installazione del seguente plugin fallita: %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>Collegamento perso con %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>collegato con %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ja.ts b/src/mumble/mumble_ja.ts
index 00cbf7ba6..add00ec4f 100644
--- a/src/mumble/mumble_ja.ts
+++ b/src/mumble/mumble_ja.ts
@@ -3826,6 +3826,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6170,12 +6174,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6495,10 +6500,6 @@ Valid options are:
<translation>起動時に最後に接続したサーバに再接続する</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>開始時にプラグインとオーバレイの更新をダウンロードする</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6523,6 +6524,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7077,31 +7086,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名前</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>有効化</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>デバッグモードではプラグインのアップデートはスキップします。</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>更新されたプラグインを %1 にダウンロードしました。</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1 に新しいプラグインをインストールできません。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 はリンクを失いました。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 はリンクされました。</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ko.ts b/src/mumble/mumble_ko.ts
index 6cd8662fe..420312974 100644
--- a/src/mumble/mumble_ko.ts
+++ b/src/mumble/mumble_ko.ts
@@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6152,12 +6156,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6478,10 +6483,6 @@ Valid options are:
<translation>시작시 마지막으로 접속한 서버에 다시 접속한다</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>시작시 플러그 인과 오버레이의 업데이트를 다운로드한다.</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6506,6 +6507,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7060,31 +7069,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>이름</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>유효</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>디버깅 모드에서는 플러그인의 업데이트가 스킵 됩니다.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>업데이트된 플러그인을 %1에 다운 받았습니다.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1에 새로운 플러그인을 설치할 수 없습니다.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1은 링크가 소실 되었습니다.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1은 링크되었습니다.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_lt.ts b/src/mumble/mumble_lt.ts
index abd115c97..4c7d4c039 100644
--- a/src/mumble/mumble_lt.ts
+++ b/src/mumble/mumble_lt.ts
@@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6154,12 +6158,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6479,10 +6484,6 @@ Valid options are:
<translation>Paleidus programą, iš naujo prisijungti prie paskutinio serverio</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privatumas</translation>
</message>
@@ -6507,6 +6508,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7061,30 +7070,188 @@ Norėdami naujinti šiuos failus į naujausią versiją, spustelėkite mygtuką
<translation>Pavadinimas</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Įjungta</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Įjungti</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Derinimo veiksenoje, praleidžiamas įskiepio atnaujinimas.</translation>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Atsisiųstas naujas ar atnaujintas įskiepis į %1.</translation>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nepavyko įdiegti naujo įskiepio į %1.</translation>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_nl.ts b/src/mumble/mumble_nl.ts
index 774988679..894779254 100644
--- a/src/mumble/mumble_nl.ts
+++ b/src/mumble/mumble_nl.ts
@@ -3838,6 +3838,10 @@ Veld beschrijft LCD-apparaatgrootte aangeduid in pixels (voor Grafische LCD&apos
<source>User stopped listening to channel</source>
<translation>Gebruiker luistert niet meer naar kanaal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6214,12 +6218,13 @@ Valide acties zijn:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6269,60 +6274,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Gebruik: mumble [opties] [&lt;url&gt;]
-
-&lt;url&gt; specifieert een URL om mee te verbinden bij het opstarten i.p.v.
-het venster om te verbinden te tonen, en heeft het volgende formaat:
-mumble://[&lt;gebruikersnaam&gt;[:&lt;wachtwoord&gt;]@]&lt;serveradres&gt;[:&lt;poort&gt;][/&lt;kanaal&gt;[/&lt;subkanaal&gt;...]][?versie=&lt;x.y.z&gt;]
-
-De versie-parameter moet ingesteld worden om de correcte versie
-van Mumble te kiezen. Momenteel is dit standaard 1.2.0.
-
-Valide opties zijn:
- -h, --help Toont deze hulptekst en sluit vervolgens af.
- -m, --multiple
- Laat toe dat meerdere instanties van de applicatie tegelijk draaien.
- -c, --config
- Geef een alternatief configuratiebestand op.
- Als je dit gebruikt om meerdere instanties van Mumble tegelijk te draaien,
- vergeet dan niet in dit bestand een aparte waarde voor &apos;database&apos; op te geven.
- -n, --noidentity
- Voorkom het laden van identiteitsbestanden (bv. certificaten).
- -jn, --jackname &lt;argument&gt;
- Stel een zelfgekozen client-naam in voor Jack.
- --license
- Toon de licentie van Mumble.
- --authors
- Toon een overzicht van de auteurs van Mumble.
- --third-party-licenses
- Toon licenties van software van derde partijen die gebruikt wordt door Mumble.
- --window-title-ext &lt;argument&gt;
- Stelt een zelfgekozen achtervoegsel in voor de titel van het venster.
- --dump-input-streams
- Dump PCM streams op verschillende plaatsen gedurende de invoerverwerking
- (nuttig bij het opsporen van bugs)
- - Onverwerkte microfooninvoer
- - Teruglezen van spraak bij echo-opheffing
- - Verwerkte microfooninvoer
- --print-echocancel-queue
- Stuur de staat van de wachtrij die gebruikt wordt voor echo-opheffing naar stdout
- (nuttig bij het opsporen van bugs)
- --translation-dir &lt;map&gt;
- Geeft een bijkomende vertalingsmap &lt;map&gt; op waarin Mumble moet zoeken op
- vertalingsbestanden, die vervolgens voorrang krijgen op de ingebouwde.
- Deze mappen krijgen een hogere prioriteit dan de standaardlocaties die anders
- gebruikt worden.
- --print-translation-dirs
- Print de paden die Mumble doorzoekt voor vertalingsbestanden die voorrang
- krijgen op de ingebouwde vertalingen af.
- (Nuttig voor vertalers die hun vertalingen willen testen)
- --locale &lt;taalgebied&gt;
- Overschrijf het taalgebied uit de instellingen van Mumble met een taalgebied
- dat overeenkomt met de opgegeven identificator.
- Als het formaat ongeldig is, zal Mumble een fout geven.
- Indien er geen fout is, zal dit taalgebied permanent opgeslagen worden in de
- instellingen van Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6592,10 +6544,6 @@ Valide opties zijn:
<translation>Verbind opnieuw met laatst bezochte server bij opstarten</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Plug-in/nieuwe overlay-updates downloaden bij starten</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacy</translation>
</message>
@@ -6621,6 +6569,14 @@ Voorkomt dat Mumble potentieel identificerende informatie over het besturingssys
<source>Hide public server list</source>
<translation>Verberg publieke server-lijst</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7175,31 +7131,189 @@ Klik op de onderstaande knop om deze bestanden naar de laatste versie bij te wer
<translation>Naam</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Geactiveerd</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Activeren</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Plugins bijwerken overslaan in foutopsporende-modus.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Nieuwe of bijgewerkte plug-in naar %1 gedownload.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nieuwe plug-in naar %1 installeren mislukt.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 verloor verbinding.</translation>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>PluginUpdater</name>
<message>
- <source>%1 linked.</source>
- <translation>%1 verbonden.</translation>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_no.ts b/src/mumble/mumble_no.ts
index 62db26e34..07486d581 100644
--- a/src/mumble/mumble_no.ts
+++ b/src/mumble/mumble_no.ts
@@ -3850,6 +3850,10 @@ Dette feltet beskriver størrelsen på en LCD-enhet. Enten gitt i piksler (for g
<source>User stopped listening to channel</source>
<translation>Bruker stoppet å lytte til kanalen</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6204,12 +6208,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6529,10 +6534,6 @@ Valid options are:
<translation>Koble til tjeneren som sist ble brukt ved oppstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Last ned programtillegg og forgrunnsinformasjonsoppdateringer ved oppstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Personvern</translation>
</message>
@@ -6558,6 +6559,14 @@ Forhindrer klienten fra å sende potensielt identifiserende informasjon om opera
<source>Hide public server list</source>
<translation>Skjul offentlig tjenerliste</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7112,31 +7121,189 @@ Trykk på knappen nedefor for å oppgradere.</translation>
<translation>Navn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Påskrudd</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Skru på</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hopper over programoppdatering i feilrettingsmodus.</translation>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Programtillegg lastet ned eller oppdatert til %1.</translation>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Klarte ikke å installere nytt programtillegg til %1.</translation>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 mistet lenke.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 er lenket.</translation>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_oc.ts b/src/mumble/mumble_oc.ts
index 34f7cec51..c5b0ce572 100644
--- a/src/mumble/mumble_oc.ts
+++ b/src/mumble/mumble_oc.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nom</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_pl.ts b/src/mumble/mumble_pl.ts
index 65267a939..05d9c5b9b 100644
--- a/src/mumble/mumble_pl.ts
+++ b/src/mumble/mumble_pl.ts
@@ -3839,6 +3839,10 @@ Te pole opisuje rozmiar urządzenia LCD. Rozmiar jest podany w pikselach lub w z
<source>User stopped listening to channel</source>
<translation>Użytkownik przestał słuchać kanału</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6215,12 +6219,13 @@ toggledeaf
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6270,61 +6275,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Użycie: mumble [opcje] [&lt;url&gt;]
-
-&lt;url&gt; określa adres URL, z którym należy się połączyć po uruchomieniu zamiast pokazywać
-okno połączenia, ma następującą postać:
-mumble://[&lt;nazwa użytkownika&gt;[:&lt;hasło&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;kanał&gt;[/&lt;podkanał&gt;...]][?wersja=&lt;x.y.z&gt;]
-
-Aby wywołać poprawną wersję klienta, należy ustawić parametr
-zapytania o wersję. Obecnie domyślnie jest to 1.2.0.
-
-Prawidłowe opcje to:
- -h, --help Pokaż ten tekst pomocy i zakończ.
- -m, --multiple
- Zezwalaj na uruchamianie wielu instancji klienta.
- -c, --config
- Określ alternatywny plik konfiguracyjny.
- Jeśli używasz tego do uruchamiania wielu wystąpień Mumble jednocześnie,
- upewnij się, że ustawiono alternatywną wartość „bazy danych” w konfiguracji.
- -n, --noidentity
- Blokuj ładowanie plików tożsamości (tj. certyfikatów).
- -jn, --jackname &lt;arg&gt;
- Ustaw niestandardową nazwę klienta Jack.
- --license
- Pokaż licencję Mumble.
- --authors
- Pokaż autorów Mumble.
- --third-party-licenses
- Pokaż licencje na oprogramowanie innych firm używane przez Mumble.
- --window-title-ext &lt;arg&gt;
- Ustawia niestandardowe rozszerzenie tytułu okna.
- --dump-input-stream
- Zrzuca strumienie PCM w różnych częściach łańcucha wejściowego
- (przydatne do debugowania)
- - surowe wejście mikrofonowe
- - odczyt głośnika w celu usunięcia echa
- - przetworzone wejście mikrofonowe
- --print-echocancel-queue
- Wyświetl na stdout stan kolejki anulowania echa
- (przydatne do debugowania)
- --translation-dir &lt;dir&gt;
- Określa dodatkowe tłumaczenie dla &lt;kat&gt;, w którym
- Mumble wyszuka pliki tłumaczeń, które nadpiszą
- dołączone
- Katalogi dodane w ten sposób mają wyższy priorytet niż
- domyślne lokalizacje używane w inny sposób
- --print-translation-dirs
- Wyświetl ścieżki, których będzie szukał Mumble
- plików tłumaczeń, które zastępują dołączone pliki.
- (Przydatne dla tłumaczy testujących swoje tłumaczenia)
- --locale &lt;ust. reg,&gt;
- Nadpisz ustawienia regionalne w ustawieniach Mumble za pomocą
- ustawień reg., które odpowiada podanemu łańcuchowi ustawień reg.
- Jeśli format jest nieprawidłowy, Mumble wyświetli błąd.
- W przeciwnym razie ustawienia regionalne zostaną trwale zapisane w
- ustawieniach Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6594,10 +6545,6 @@ Prawidłowe opcje to:
<translation>Przy starcie połącz ponownie do ostatniego serwera</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Pobieraj aktualizacje nakładki oraz wtyczek przy starcie</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Prywatność</translation>
</message>
@@ -6623,6 +6570,14 @@ Uniemożliwia klientowi wysyłanie potencjalnie identyfikujących informacji o s
<source>Hide public server list</source>
<translation>Ukryj listę serwerów publicznych</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7177,31 +7132,189 @@ Aby uaktualnić pliki do najnowszych wersji, kliknij przycisk poniżej.</transla
<translation>Nazwa</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Włączona</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Włącz</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Pomijanie aktualizacji wtyczek w trybie debugowania.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Pobrano nowy lub uaktualniony plugin do %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nie udało się zainstalować nowego pluginu do %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 utracił połączenie.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 połączony.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_pt_BR.ts b/src/mumble/mumble_pt_BR.ts
index 451b875f6..e5e34bf6f 100644
--- a/src/mumble/mumble_pt_BR.ts
+++ b/src/mumble/mumble_pt_BR.ts
@@ -3838,6 +3838,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels
<source>User stopped listening to channel</source>
<translation>Usuário parou de ouvir o canal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6192,12 +6196,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6517,10 +6522,6 @@ Valid options are:
<translation>Reconectar ao último servidor ao iniciar</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Baixar atualizações de complementos e sobreimpressão ao iniciar</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacidade</translation>
</message>
@@ -6546,6 +6547,14 @@ Evita que o cliente envie informações potencialmente capazes de identificaçã
<source>Hide public server list</source>
<translation>Ocultar lista de servidores públicos</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7100,31 +7109,189 @@ Para atualizar estes arquivos para suas últimas versões, clique no botão abai
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ativo</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Ativar</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Pulando atualização de complementos no modo de depuração.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Baixou complemento novo ou atualizado para %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Falha ao instalar a novo complemento para %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdeu vínculo.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 vinculado.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_pt_PT.ts b/src/mumble/mumble_pt_PT.ts
index dfa50ea20..f683c817d 100644
--- a/src/mumble/mumble_pt_PT.ts
+++ b/src/mumble/mumble_pt_PT.ts
@@ -3824,6 +3824,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6175,12 +6179,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6500,10 +6505,6 @@ Valid options are:
<translation>Ligar novamente ao último servidor ao iniciar</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Tranferir atualizações de plugins e sobreposição ao iniciar</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6528,6 +6529,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7082,31 +7091,189 @@ Para atualizar estes ficheiros para suas últimas versões, clique no botão aba
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ativo</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Saltar atualização de plugin no modo de depuração.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Transferido plugin novo ou atualizado para %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Falha ao instalar a novo plugin para %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdeu ligação.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 ligado.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ro.ts b/src/mumble/mumble_ro.ts
index ce4203450..be4b70950 100644
--- a/src/mumble/mumble_ro.ts
+++ b/src/mumble/mumble_ro.ts
@@ -3783,6 +3783,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6126,12 +6130,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6451,10 +6456,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6479,6 +6480,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7029,30 +7038,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nume</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_ru.ts b/src/mumble/mumble_ru.ts
index a1957a4a2..007d7a382 100644
--- a/src/mumble/mumble_ru.ts
+++ b/src/mumble/mumble_ru.ts
@@ -3792,6 +3792,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation> Пользователь перестал слушать канал</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6168,12 +6172,13 @@ Valid actions are:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6493,10 +6498,6 @@ Valid options are:
<translation>Подключаться к последнему серверу при запуске</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Загружать обновления плагинов и табло при запуске</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Конфиденциальность</translation>
</message>
@@ -6521,6 +6522,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>Скрыть список публичных серверов</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Имя</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Включено</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Включить</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Пропустить обновление плагина в режиме отладки.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Загрузка нового или обновленного плагина в %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Ошибка при установке нового плагина в %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 потерял связь.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 подключен.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_si.ts b/src/mumble/mumble_si.ts
index 21842df7b..967c0a939 100644
--- a/src/mumble/mumble_si.ts
+++ b/src/mumble/mumble_si.ts
@@ -3753,6 +3753,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>%1 link</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6084,12 +6088,13 @@ Otherwise abort and check your certificate and username.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6409,10 +6414,6 @@ Prevents the client from sending potentially identifying information about the o
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Submit anonymous statistics to the Mumble project</source>
<translation type="unfinished"></translation>
</message>
@@ -6436,6 +6437,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Network</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -6939,10 +6948,6 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"></translation>
</message>
<message>
- <source>Enabled</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Reloads all plugins</source>
<translation type="unfinished"></translation>
</message>
@@ -6986,27 +6991,189 @@ To upgrade these files to their latest versions, click the button below.</source
<source>Plugin has no about function.</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>%1 lost link.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_sv.ts b/src/mumble/mumble_sv.ts
index d6df92fe9..3f4d1708c 100644
--- a/src/mumble/mumble_sv.ts
+++ b/src/mumble/mumble_sv.ts
@@ -3838,6 +3838,10 @@ Detta fält beskriver storleken av en LCD-enhet. Storleken mäts i pixlar (för
<source>User stopped listening to channel</source>
<translation>Anändare slutade lyssna på kanalen</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6214,12 +6218,13 @@ Giltiga åtgärder är:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6269,61 +6274,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Användning: mumble [options] [&lt;url&gt;]
-
-&lt;url&gt; anger en URL som ska anslutas till efter uppstart istället för att visa
-anslutningsfönstret, och har följande form:
-mumble://[&lt;användarnamn&gt;[:&lt;lösenord&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;kanal&gt;[/&lt;underkanal&gt;...]][?version=&lt;x.y.z&gt;]
-
-Förfrågningsparametern version måste ställas in för att kunna åberopa
-rätt klientversion. För närvarande är standardvärdet 1.2.0.
-
-Giltiga alternativ är:
- -h, --help Visar denna hjälptext.
- -m, --multiple
- Tillåt att flera instanser av klienten startas.
- -c, --config
- Ange en alternativ konfigurationsfil.
- Om du använder detta för att köra flera instanser av Mumble på en gång,
- se till att ställa in ett alternativt &quot;databas&quot; -värde i konfigurationen.
- -n, --noidentity
- Dämpa inläsning av identitetsfiler (dvs. certifikat.)
- -jn, --jackname &lt;arg&gt;
- Ange anpassat Jack-klientnamn.
- --license
- Visa Mumble-licensen.
- --authors
- VIsa Mumble-skapare.
- --third-party-licenses
- Visa licenser för tredje-partmjukvara som används av Mumble.
- --window-title-ext &lt;arg&gt;
- Ställer in ett anpassat fönstertiteltillägg.
- --dump-input-streams
- Dumpa PCM-strömmar vid olika delar av ingångskedjan
- (användbart för felsökningsändamål)
- - rå mikrofoningång
- - högtalaravläsning för ekodämpning
- - bearbetad mikrofoningång
- --print-echocancel-queue
- Skriv ut på stdout ekoställningskön
- (användbart för felsökningsändamål)
---translation-dir &lt;dir&gt;
- Anger en ytterligare översättning för &lt;dir&gt; där
- Mumble söker efter översättningsfiler som skrivs över
- de medföljande
- kataloger som läggs till på detta sätt har högre prioritet än
- standardplatserna som används annars
---print-translation-dirs
- Skriv ut de vägar som Mumble söker efter
- översättningsfiler som skriver över de medföljande filerna.
- (Användbart för översättare som testar sina översättningar)
- --locale &lt;locale&gt;
- Skriver över lokaladressen i Mumbles inställningar med en
- lokal som motsvarar den angivna lokalsträngen.
- Om formatet är ogiltigt kommer Mumble att göra ett fel.
- Annars sparas språket permanent i
- Mumbles inställningar.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6593,10 +6544,6 @@ Giltiga alternativ är:
<translation>Återanslut till den senaste servern vid uppstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Ladda ner uppdateringar för insticksmoduler och överlag vid uppstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Sekretess</translation>
</message>
@@ -6622,6 +6569,14 @@ Förhindrar klienten från att skicka potentiellt identifierande information om
<source>Hide public server list</source>
<translation>Dölj offentlig serverlista</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7176,31 +7131,189 @@ Tryck på knappen nedan för att uppgradera dessa filer till de senaste versione
<translation>Namn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiverad</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Aktivera</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hoppar över uppdatering för insticksmoduler i felsökningsläge.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Laddade ner ny/uppdaterad insticksmodul till %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Kude inte installera ny insticksmodul till %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 förlorade länk.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 länkad.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_te.ts b/src/mumble/mumble_te.ts
index 5615b89c2..3c894a928 100644
--- a/src/mumble/mumble_te.ts
+++ b/src/mumble/mumble_te.ts
@@ -3790,6 +3790,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6133,12 +6137,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6464,10 +6469,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6492,6 +6493,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7042,30 +7051,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"> నామము</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_th.ts b/src/mumble/mumble_th.ts
index c85c22f6d..bcff56ff3 100644
--- a/src/mumble/mumble_th.ts
+++ b/src/mumble/mumble_th.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">ชื่อ</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_tr.ts b/src/mumble/mumble_tr.ts
index 8c44ac139..524a04bf1 100644
--- a/src/mumble/mumble_tr.ts
+++ b/src/mumble/mumble_tr.ts
@@ -3836,6 +3836,10 @@ Bu alan LCD aygıtın boyutunu belirtir. Boyut ya piksel olarak (Grafik LCD ekra
<source>User stopped listening to channel</source>
<translation>Kullanıcı kanalı dinlemeye son verdi</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6188,12 +6192,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6513,10 +6518,6 @@ Valid options are:
<translation>Başladığında son sunucuya bağlan</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Başladığında eklenti ve yerpaylaşan güncellemelerini indir</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Gizlilik</translation>
</message>
@@ -6542,6 +6543,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7096,31 +7105,189 @@ Bu dosyaları son sürümlerine güncellemek için aşağıdaki düğmeyi tıkla
<translation>İsim</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Etkin</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hata ayıklama kipinde eklenti güncellemesi atlanıyor.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Yeni ya da güncelleştirilmiş eklenti %1 konumuna indirildi.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Yeni eklenti %1 konumuna kurulamadı.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 bağlantı kaybetti.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 bağlandı.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_uk.ts b/src/mumble/mumble_uk.ts
index c58ae0fd3..3b075ef7e 100644
--- a/src/mumble/mumble_uk.ts
+++ b/src/mumble/mumble_uk.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Ім&apos;я</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_zh_CN.ts b/src/mumble/mumble_zh_CN.ts
index d3527a07f..df68662bb 100644
--- a/src/mumble/mumble_zh_CN.ts
+++ b/src/mumble/mumble_zh_CN.ts
@@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>用户停止监听频道</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6212,12 +6216,13 @@ Valid actions are:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6267,54 +6272,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>用法:mumble [选项] [&lt;URL&gt;]
-
-&lt;URL&gt; 指定启动时连接的 URL,而不是显示连接窗口,URL 的格式为:
-mumble://[&lt;用户名&gt;[:&lt;密码&gt;]@]&lt;主机名&gt;[:&lt;端口&gt;][/&lt;频道名&gt;[/&lt;子频道名&gt;...]][?version=&lt;x.y.z&gt;]
-
-必须设置 version 请求参数以调用正确的客户端版本,当前的默认值为 1.2.0。
-
-可用的选项:
- -h, --help 显示此帮助信息并退出。
- -m, --multiple
- 允许启动多个客户端实例。
- -c, --config
- 指定替代配置文件。
- 如果您使用此参数同时运行多个 Mumble 实例,
- 请确保在配置文件内设置替代 &apos;database&apos; 选项。
- -n, --noidentity
- 禁止加载身份认证文件(即证书)。
- -jn, --jackname &lt;参数&gt;
- 设置自定义 Jack 客户端名称。
- --license
- 显示 Mumble 许可。
- --authors
- 显示 Mumble 作者。
- --third-party-licenses
- 显示 Mumble 使用的第三方软件的许可。
- --window-title-ext &lt;参数&gt;
- 设置自定义窗口标题后缀名。
- --dump-input-streams
- 转储输入链上各部分的 PCM 流。
- (适用于调试目的)
- - 原始麦克风输入
- - 扬声器回声消除重读取
- - 已处理麦克风输入
- --print-echocancel-queue
- 向标准输出打印回声消除队列状态。
- (适用于调试目的)
- --translation-dir &lt;目录&gt;
- 指定一个额外的目录,Mumble 会在其中搜索翻译文件
- 来覆盖内置的翻译。通过此方式添加的目录比其它情况
- 下的默认位置具有更高的优先级。
- --print-translation-dirs
- 输出 Mumble 会在哪些目录搜索翻译文件以覆盖内置翻译。
- (适用于译者测试自己的翻译)
- --locale &lt;区域语言代码&gt;
- 用指定字符串对应的语言覆盖 Mumble 的语言设置。
- 如果字符串格式无效,Mumble 会出错。
- 否则,指定的语言会永久保存到 Mumble 设置中。
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6584,10 +6542,6 @@ mumble://[&lt;用户名&gt;[:&lt;密码&gt;]@]&lt;主机名&gt;[:&lt;端口&gt;]
<translation>启动时自动连接上次的服务器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>启动时下载插件和游戏内界面更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>隐私</translation>
</message>
@@ -6613,6 +6567,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>隐藏公共服务器列表</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7167,31 +7129,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名称</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>启用</translation>
+ <source>Enable</source>
+ <translation type="unfinished">启用</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在调试模式跳过插件更新。</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下载新版或升级插件到 %1。</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安装新插件到 %1 失败。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 链接丢失。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已链接。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_zh_HK.ts b/src/mumble/mumble_zh_HK.ts
index 3f4d56f71..3cfe84124 100644
--- a/src/mumble/mumble_zh_HK.ts
+++ b/src/mumble/mumble_zh_HK.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation>啟動時自動連接到最後使用的伺服器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>啟動時下載外掛與浮動視窗的更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7032,31 +7041,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名稱</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>啟用</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在除錯模式中忽略外掛更新訊息。</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下載或更新 %1 外掛。</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安裝 %1 外掛失敗。</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 失去關聯。</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已關聯。</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_zh_TW.ts b/src/mumble/mumble_zh_TW.ts
index 2ff9d3f8b..979b4f3c8 100644
--- a/src/mumble/mumble_zh_TW.ts
+++ b/src/mumble/mumble_zh_TW.ts
@@ -3805,6 +3805,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6148,12 +6152,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6473,10 +6478,6 @@ Valid options are:
<translation>啟動時自動連接到最後使用的伺服器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>啟動時下載外掛與浮動視窗的更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6501,6 +6502,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7054,31 +7063,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名稱</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>已啟用</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在除錯模式忽略外掛更新訊息。</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下載或更新 %1 外掛。</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安裝 %1 外掛失敗。</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 連接遺失。</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已連接。</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>