Compare commits
32 Commits
v1.7.0
...
dependency
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e939c4592 | ||
|
|
27faed4a3a | ||
|
|
10fff86ca4 | ||
|
|
87e84dec5c | ||
|
|
4b782133ae | ||
|
|
d5257207bc | ||
|
|
423884ae0c | ||
|
|
7ea3d0aab1 | ||
|
|
18b15f5cf4 | ||
|
|
96bca52d0d | ||
|
|
3540774c82 | ||
|
|
b5435acdd8 | ||
|
|
38c2b92e2d | ||
|
|
e447206082 | ||
|
|
77f31b63aa | ||
|
|
0b9fc100c2 | ||
|
|
f73524de27 | ||
|
|
8a8690fe53 | ||
|
|
259e6dc75d | ||
|
|
9cbdca0441 | ||
|
|
a9ddc0216f | ||
|
|
b39e23d462 | ||
|
|
331fb8f4ca | ||
|
|
335a4456ed | ||
|
|
26a2ca7b05 | ||
|
|
30c0526ff7 | ||
|
|
3119e1df19 | ||
|
|
63180f5d53 | ||
|
|
78d43991ab | ||
|
|
193b69f45f | ||
|
|
485ff9634b | ||
|
|
74fdef683f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
.vscode
|
.vscode
|
||||||
|
/submodules/arrpc
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ set(discord-screenaudio_SRC
|
|||||||
src/streamdialog.cpp
|
src/streamdialog.cpp
|
||||||
src/log.cpp
|
src/log.cpp
|
||||||
src/userscript.cpp
|
src/userscript.cpp
|
||||||
|
src/centralwidget.cpp
|
||||||
|
src/dependencycheck.cpp
|
||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -9,7 +9,7 @@ of [@edisionnano](https://github.com/edisionnano) and the
|
|||||||
|
|
||||||
Unlike a lot of other solutions, the audio here is directly fed into the
|
Unlike a lot of other solutions, the audio here is directly fed into the
|
||||||
screenshare and not passed to the user microphone
|
screenshare and not passed to the user microphone
|
||||||
([see explanation](#how-it-works)).
|
([see explanation](#how-does-this-work)).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -106,9 +106,11 @@ allowing access to "All system files" under the "Filesystem" section.
|
|||||||
### Is there any way to add custom CSS / a theme?
|
### Is there any way to add custom CSS / a theme?
|
||||||
|
|
||||||
Yes, you can add all your styles into
|
Yes, you can add all your styles into
|
||||||
`~/.config/discord-screenaudio/userstyles.css`. But please note that due to
|
`~/.config/discord-screenaudio/userstyles.css` (or
|
||||||
QtWebEngine limitations concerning content security policies, you can't use any
|
`~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css`
|
||||||
external files (like `@import` or `url()`).
|
if you are using the Flatpak). But please note that due to QtWebEngine
|
||||||
|
limitations concerning content security policies, you can't use any external
|
||||||
|
files (like `@import` or `url()`).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
1
assets/arrpc.js
Normal file
1
assets/arrpc.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/arrpc_bridge_mod.js
Normal file
1
assets/arrpc_bridge_mod.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
setTimeout((i=>{let t,a,s,e={};new WebSocket("ws://127.0.0.1:1337").onmessage=async i=>{if(msg=JSON.parse(i.data),console.log(msg),!t){const i=window.webpackChunkdiscord_app.push([[Symbol()],{},i=>i]),e=i.c;window.webpackChunkdiscord_app.pop();for(const i in e){let a=e[i].exports;if(a=a&&(a.Z??a.ZP),a&&a.register&&a.wait){t=a;break}}const n=i.m;for(const t in n)if(n[t].toString().includes("getAssetImage: size must === [number, number] for Twitch")){const s=i(t),e=Object.values(s).find((i=>"function"==typeof i&&i.toString().includes("apply(")));a=async(i,t)=>(await e(i,[t,void 0]))[0];break}for(const t in n)if(n[t].toString().includes("e.application={")){const a=i(t),e=Object.values(a).find((i=>"function"==typeof i&&i.toString().includes("e.application={")));s=async i=>{let t={};return await e(t,i),t.application};break}}if(msg.activity?.assets?.large_image&&(msg.activity.assets.large_image=await a(msg.activity.application_id,msg.activity.assets.large_image)),msg.activity?.assets?.small_image&&(msg.activity.assets.small_image=await a(msg.activity.application_id,msg.activity.assets.small_image)),msg.activity){const i=msg.activity.application_id;e[i]||(e[i]=await s(i));const t=e[i];msg.activity.name||(msg.activity.name=t.name)}t.dispatch({type:"LOCAL_ACTIVITY_UPDATE",...msg})}}),1e4);
|
||||||
@@ -139,10 +139,12 @@ setTimeout(() => {
|
|||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
userscript.muteToggled.connect(() => {
|
userscript.muteToggled.connect(() => {
|
||||||
|
console.log("Toggling mute");
|
||||||
muteBtn && muteBtn.click();
|
muteBtn && muteBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
userscript.deafenToggled.connect(() => {
|
userscript.deafenToggled.connect(() => {
|
||||||
|
console.log("Toggling deafen");
|
||||||
deafenBtn && deafenBtn.click();
|
deafenBtn && deafenBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,6 +159,25 @@ function main() {
|
|||||||
streamStartBtnClone.remove();
|
streamStartBtnClone.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateUserstyles() {
|
||||||
|
userscript.log("Loading userstyles...");
|
||||||
|
userscript.loadingMessage = "Loading userstyles...";
|
||||||
|
let stylesheet = document.getElementById("discordScreenaudioUserstyles");
|
||||||
|
if (stylesheet) {
|
||||||
|
userscript.log("Removing old userstyles...");
|
||||||
|
stylesheet.remove();
|
||||||
|
}
|
||||||
|
stylesheet = document.createElement("style");
|
||||||
|
stylesheet.id = "discordScreenaudioUserstyles";
|
||||||
|
stylesheet.innerText = userscript.userstyles;
|
||||||
|
document.head.appendChild(stylesheet);
|
||||||
|
userscript.log("Finished loading userstyles");
|
||||||
|
userscript.loadingMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
userscript.userstylesChanged.connect(updateUserstyles);
|
||||||
|
setTimeout(() => updateUserstyles());
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const streamActive =
|
const streamActive =
|
||||||
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
||||||
@@ -266,15 +287,11 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
muteBtn = buttonContainer
|
muteBtn = buttonContainer
|
||||||
? buttonContainer.getElementsByClassName(
|
? buttonContainer.getElementsByTagName("button")[0]
|
||||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
|
||||||
)[0]
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
deafenBtn = buttonContainer
|
deafenBtn = buttonContainer
|
||||||
? buttonContainer.getElementsByClassName(
|
? buttonContainer.getElementsByTagName("button")[1]
|
||||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
|
||||||
)[1]
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (resolutionString) {
|
if (resolutionString) {
|
||||||
@@ -311,6 +328,18 @@ function main() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// section.appendChild(
|
||||||
|
// createButton("Install Theme", () => {
|
||||||
|
// userscript.showThemeDialog();
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
|
// section.appendChild(
|
||||||
|
// createButton("Uninstall Theme", () => {
|
||||||
|
// userscript.installUserStyles("");
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
section.appendChild(
|
section.appendChild(
|
||||||
createSwitch(
|
createSwitch(
|
||||||
"Move discord-screenaudio to the system tray instead of closing",
|
"Move discord-screenaudio to the system tray instead of closing",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource>
|
<qresource>
|
||||||
<file>assets/userscript.js</file>
|
<file>assets/userscript.js</file>
|
||||||
|
<file>assets/arrpc_bridge_mod.js</file>
|
||||||
|
<file>assets/arrpc.js</file>
|
||||||
<file>assets/de.shorsh.discord-screenaudio.png</file>
|
<file>assets/de.shorsh.discord-screenaudio.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
36
scripts/build_arrpc.sh
Executable file
36
scripts/build_arrpc.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../submodules"
|
||||||
|
|
||||||
|
echo_status() {
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
echo "-> $1..."
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -d "arrpc" ]; then
|
||||||
|
echo_status "Cloning arRPC"
|
||||||
|
git clone https://github.com/OpenAsar/arrpc.git
|
||||||
|
cd arrpc
|
||||||
|
else
|
||||||
|
echo_status "Fetching arRPC changes"
|
||||||
|
cd arrpc
|
||||||
|
git fetch
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo_status "Checking out latest commit"
|
||||||
|
git reset --hard HEAD
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
echo_status "Installing dependencies"
|
||||||
|
pnpm i -D @vercel/ncc
|
||||||
|
|
||||||
|
echo_status "Patching arRPC"
|
||||||
|
sed -i 's/"type": "module",//' package.json
|
||||||
|
|
||||||
|
echo_status "Building arRPC"
|
||||||
|
pnpm exec ncc build -m src/index.js
|
||||||
|
|
||||||
|
echo_status "Copying built file"
|
||||||
|
cp -v ./dist/index.js ../../assets/arrpc.js
|
||||||
84
src/centralwidget.cpp
Normal file
84
src/centralwidget.cpp
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#include "centralwidget.h"
|
||||||
|
#include "discordpage.h"
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
|
#include <QWebEngineNotification>
|
||||||
|
#include <QWebEngineProfile>
|
||||||
|
#include <QWebEngineScript>
|
||||||
|
#include <QWebEngineScriptCollection>
|
||||||
|
#include <QWebEngineSettings>
|
||||||
|
|
||||||
|
CentralWidget::CentralWidget(QWidget *parent) : QWidget(parent) {
|
||||||
|
setStyleSheet("background-color:#313338;");
|
||||||
|
m_layout = new QVBoxLayout(this);
|
||||||
|
m_layout->setMargin(0);
|
||||||
|
m_layout->setSpacing(0);
|
||||||
|
setupWebView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CentralWidget::setupWebView() {
|
||||||
|
auto page = new DiscordPage(this);
|
||||||
|
|
||||||
|
m_webView = new QWebEngineView(this);
|
||||||
|
m_webView->setPage(page);
|
||||||
|
|
||||||
|
bool useNotifySend = MainWindow::instance()
|
||||||
|
->settings()
|
||||||
|
->value("useNotifySend", false)
|
||||||
|
.toBool();
|
||||||
|
if (m_useKF5Notifications || useNotifySend)
|
||||||
|
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
|
||||||
|
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
|
||||||
|
if (useNotifySend) {
|
||||||
|
auto title = notificationInfo->title();
|
||||||
|
auto message = notificationInfo->message();
|
||||||
|
auto image_path =
|
||||||
|
QString("/tmp/discord-screenaudio-%1.png").arg(title);
|
||||||
|
notificationInfo->icon().save(image_path);
|
||||||
|
QProcess::execute("notify-send",
|
||||||
|
{"--icon", image_path, "--app-name",
|
||||||
|
"discord-screenaudio", title, message});
|
||||||
|
} else if (m_useKF5Notifications) {
|
||||||
|
#ifdef KNOTIFICATIONS
|
||||||
|
KNotification *notification =
|
||||||
|
new KNotification("discordNotification");
|
||||||
|
notification->setTitle(notificationInfo->title());
|
||||||
|
notification->setText(notificationInfo->message());
|
||||||
|
notification->setPixmap(
|
||||||
|
QPixmap::fromImage(notificationInfo->icon()));
|
||||||
|
notification->setDefaultAction("View");
|
||||||
|
connect(notification, &KNotification::defaultActivated,
|
||||||
|
[&, notificationInfo = std::move(notificationInfo)]() {
|
||||||
|
notificationInfo->click();
|
||||||
|
show();
|
||||||
|
activateWindow();
|
||||||
|
});
|
||||||
|
notification->sendEvent();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(page->userScript(), &UserScript::loadingMessageChanged, this,
|
||||||
|
&CentralWidget::setLoadingIndicator);
|
||||||
|
|
||||||
|
m_layout->addWidget(m_webView);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CentralWidget::setLoadingIndicator(QString text) {
|
||||||
|
if (text != "") {
|
||||||
|
if (m_loadingLabel == nullptr) {
|
||||||
|
m_loadingLabel = new QLabel(this);
|
||||||
|
m_loadingLabel->setMaximumHeight(20);
|
||||||
|
m_loadingLabel->setAlignment(Qt::AlignHCenter);
|
||||||
|
m_loadingLabel->setStyleSheet("color:#dedede;");
|
||||||
|
m_layout->addWidget(m_loadingLabel);
|
||||||
|
}
|
||||||
|
m_loadingLabel->setText(text.mid(0, 100));
|
||||||
|
} else {
|
||||||
|
if (m_loadingLabel != nullptr) {
|
||||||
|
m_layout->removeWidget(m_loadingLabel);
|
||||||
|
m_loadingLabel->deleteLater();
|
||||||
|
m_loadingLabel = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/centralwidget.h
Normal file
29
src/centralwidget.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWebEnginePage>
|
||||||
|
#include <QWebEngineProfile>
|
||||||
|
#include <QWebEngineView>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class CentralWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
CentralWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupWebView();
|
||||||
|
QVBoxLayout *m_layout;
|
||||||
|
QWebEngineView *m_webView;
|
||||||
|
#ifdef KNOTIFICATIONS
|
||||||
|
bool m_useKF5Notifications = true;
|
||||||
|
#else
|
||||||
|
bool m_useKF5Notifications = false;
|
||||||
|
#endif
|
||||||
|
QLabel *m_loadingLabel = nullptr;
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void setLoadingIndicator(QString text);
|
||||||
|
};
|
||||||
10
src/dependencycheck.cpp
Normal file
10
src/dependencycheck.cpp
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#include "dependencycheck.h"
|
||||||
|
#include "pipewire-0.3/pipewire/version.h"
|
||||||
|
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
void checkDependencies() {
|
||||||
|
QString versionPipewire(pw_get_library_version());
|
||||||
|
QMessageBox::information(nullptr, "Dependency Check",
|
||||||
|
QString("Pipewire: v%1").arg(versionPipewire));
|
||||||
|
}
|
||||||
3
src/dependencycheck.h
Normal file
3
src/dependencycheck.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void checkDependencies();
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QTemporaryFile>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWebChannel>
|
#include <QWebChannel>
|
||||||
#include <QWebEngineScript>
|
#include <QWebEngineScript>
|
||||||
@@ -16,10 +17,12 @@
|
|||||||
#include <QWebEngineSettings>
|
#include <QWebEngineSettings>
|
||||||
|
|
||||||
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
||||||
setBackgroundColor(QColor("#202225"));
|
setBackgroundColor(QColor("#313338"));
|
||||||
|
|
||||||
connect(this, &QWebEnginePage::featurePermissionRequested, this,
|
connect(this, &QWebEnginePage::featurePermissionRequested, this,
|
||||||
&DiscordPage::featurePermissionRequested);
|
&DiscordPage::featurePermissionRequested);
|
||||||
|
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
|
||||||
|
&MainWindow::fullScreenRequested);
|
||||||
|
|
||||||
setupPermissions();
|
setupPermissions();
|
||||||
|
|
||||||
@@ -35,6 +38,7 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
|||||||
":/assets/userscript.js");
|
":/assets/userscript.js");
|
||||||
|
|
||||||
setupUserStyles();
|
setupUserStyles();
|
||||||
|
setupArrpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiscordPage::setupPermissions() {
|
void DiscordPage::setupPermissions() {
|
||||||
@@ -52,13 +56,79 @@ void DiscordPage::setupPermissions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DiscordPage::setupUserStyles() {
|
void DiscordPage::setupUserStyles() {
|
||||||
QString file =
|
qDebug(userstylesLog).noquote()
|
||||||
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
|
<< "Looking for userstyles in" << m_configLocation.absolutePath();
|
||||||
"/userstyles.css";
|
m_userStylesFile =
|
||||||
if (QFileInfo(file).exists()) {
|
new QFile(m_configLocation.absoluteFilePath("userstyles.css"));
|
||||||
qDebug(mainLog) << "Found userstyles:" << file;
|
if (m_userStylesFile->exists()) {
|
||||||
injectFile(&DiscordPage::injectStylesheet, "userstyles.js", file);
|
qDebug(userstylesLog).noquote()
|
||||||
|
<< "Found userstyles:" << m_userStylesFile->fileName();
|
||||||
|
m_userStylesFile->open(QIODevice::ReadOnly);
|
||||||
|
m_userStylesContent = m_userStylesFile->readAll();
|
||||||
|
m_userStylesFile->close();
|
||||||
|
fetchUserStyles();
|
||||||
}
|
}
|
||||||
|
connect(&m_userScript, &UserScript::shouldInstallUserStyles, this,
|
||||||
|
&DiscordPage::getUserStyles);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRegularExpression importRegex(
|
||||||
|
R"r(@import (?:url\(|)['"]{0,1}(?!.*usrbgs?\.css)([^'"]+?)['"]{0,1}(?:|\));)r");
|
||||||
|
const QRegularExpression urlRegex(
|
||||||
|
R"r(url\(['"]{0,1}((?!https:\/\/fonts.gstatic.com)(?!data:)(?!.*\.woff2)(?!.*\.ttf)[^'"]+?)['"]{0,1}\))r");
|
||||||
|
|
||||||
|
void DiscordPage::fetchUserStyles() {
|
||||||
|
m_userScript.setProperty(
|
||||||
|
"loadingMessage", "Loading userstyles: Fetching additional resources...");
|
||||||
|
bool foundImport = true;
|
||||||
|
auto match = importRegex.match(m_userStylesContent);
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
foundImport = false;
|
||||||
|
match = urlRegex.match(m_userStylesContent);
|
||||||
|
}
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
auto url = match.captured(1);
|
||||||
|
qDebug(userstylesLog) << "Fetching" << url;
|
||||||
|
m_userScript.setProperty(
|
||||||
|
"loadingMessage",
|
||||||
|
QString("Loading userstyles: Fetching %1...").arg(url));
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
auto reply = m_networkAccessManager.get(request);
|
||||||
|
connect(reply, &QNetworkReply::finished, [=]() {
|
||||||
|
QByteArray content = "";
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute)
|
||||||
|
.isNull())
|
||||||
|
content =
|
||||||
|
reply->attribute(QNetworkRequest::RedirectionTargetAttribute)
|
||||||
|
.toByteArray();
|
||||||
|
else
|
||||||
|
content = reply->readAll();
|
||||||
|
} else
|
||||||
|
qDebug(userstylesLog) << reply->errorString().toUtf8().constData();
|
||||||
|
reply->deleteLater();
|
||||||
|
m_userStylesContent = m_userStylesContent.replace(
|
||||||
|
match.captured(0), foundImport
|
||||||
|
? content
|
||||||
|
: "url(data:application/octet-stream;base64," +
|
||||||
|
content.toBase64() + ")");
|
||||||
|
fetchUserStyles();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qDebug(userstylesLog) << "Injecting userstyles";
|
||||||
|
m_userScript.setProperty("userstyles", m_userStylesContent);
|
||||||
|
m_userScript.setProperty("loadingMessage", "");
|
||||||
|
if (!m_configLocation.exists())
|
||||||
|
m_configLocation.mkpath(".");
|
||||||
|
m_userStylesFile->open(QIODevice::WriteOnly);
|
||||||
|
m_userStylesFile->write(m_userStylesContent.toUtf8());
|
||||||
|
m_userStylesFile->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiscordPage::getUserStyles(QString url) {
|
||||||
|
m_userStylesContent = url == "" ? "" : QString("@import url(%1);").arg(url);
|
||||||
|
fetchUserStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiscordPage::injectScript(
|
void DiscordPage::injectScript(
|
||||||
@@ -83,7 +153,6 @@ void DiscordPage::injectScript(QString name, QString content) {
|
|||||||
|
|
||||||
void DiscordPage::injectStylesheet(QString name, QString content) {
|
void DiscordPage::injectStylesheet(QString name, QString content) {
|
||||||
auto script = QString(R"(const stylesheet = document.createElement("style");
|
auto script = QString(R"(const stylesheet = document.createElement("style");
|
||||||
stylesheet.type = "text/css";
|
|
||||||
stylesheet.id = "%1";
|
stylesheet.id = "%1";
|
||||||
stylesheet.innerText = `%2`;
|
stylesheet.innerText = `%2`;
|
||||||
document.head.appendChild(stylesheet);
|
document.head.appendChild(stylesheet);
|
||||||
@@ -192,11 +261,15 @@ void DiscordPage::javaScriptConsoleMessage(
|
|||||||
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
|
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
|
||||||
int lineNumber, const QString &sourceID) {
|
int lineNumber, const QString &sourceID) {
|
||||||
auto colorSegments = message.split("%c");
|
auto colorSegments = message.split("%c");
|
||||||
|
if (colorSegments[0] != "") {
|
||||||
|
for (auto line : colorSegments[0].split("\n"))
|
||||||
|
qDebug(discordLog) << line.toUtf8().constData();
|
||||||
|
}
|
||||||
for (auto segment : colorSegments.mid(1)) {
|
for (auto segment : colorSegments.mid(1)) {
|
||||||
auto lines = segment.split("\n");
|
auto lines = segment.split("\n");
|
||||||
QString ansi;
|
QString ansi;
|
||||||
uint endOfStyles = lines.length();
|
uint endOfStyles = lines.length();
|
||||||
for (size_t line = 1; line < lines.length(); line++) {
|
for (auto line = 1; line < lines.length(); line++) {
|
||||||
if (!lines[line].endsWith(";")) {
|
if (!lines[line].endsWith(";")) {
|
||||||
endOfStyles = line;
|
endOfStyles = line;
|
||||||
break;
|
break;
|
||||||
@@ -218,3 +291,21 @@ void DiscordPage::javaScriptConsoleMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserScript *DiscordPage::userScript() { return &m_userScript; }
|
||||||
|
|
||||||
|
void DiscordPage::setupArrpc() {
|
||||||
|
QFile nodejs("/usr/bin/node");
|
||||||
|
if (nodejs.exists()) {
|
||||||
|
auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js");
|
||||||
|
qDebug(mainLog).noquote()
|
||||||
|
<< "NodeJS found, starting arRPC located at" << arrpcSource->fileName();
|
||||||
|
m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||||
|
m_arrpcProcess.setProgram(nodejs.fileName());
|
||||||
|
m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()});
|
||||||
|
m_arrpcProcess.start();
|
||||||
|
|
||||||
|
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
|
||||||
|
":/assets/arrpc_bridge_mod.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
#include "userscript.h"
|
#include "userscript.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QWebEngineFullScreenRequest>
|
#include <QWebEngineFullScreenRequest>
|
||||||
#include <QWebEnginePage>
|
#include <QWebEnginePage>
|
||||||
#include <QWebEngineScript>
|
#include <QWebEngineScript>
|
||||||
@@ -11,11 +16,20 @@ class DiscordPage : public QWebEnginePage {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit DiscordPage(QWidget *parent = nullptr);
|
explicit DiscordPage(QWidget *parent = nullptr);
|
||||||
|
UserScript *userScript();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UserScript m_userScript;
|
UserScript m_userScript;
|
||||||
|
QFile *m_userStylesFile;
|
||||||
|
QString m_userStylesContent;
|
||||||
|
QNetworkAccessManager m_networkAccessManager;
|
||||||
|
QProcess m_arrpcProcess;
|
||||||
|
const QDir m_configLocation =
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||||
void setupPermissions();
|
void setupPermissions();
|
||||||
void setupUserStyles();
|
void setupUserStyles();
|
||||||
|
void setupArrpc();
|
||||||
|
void fetchUserStyles();
|
||||||
bool acceptNavigationRequest(const QUrl &url,
|
bool acceptNavigationRequest(const QUrl &url,
|
||||||
QWebEnginePage::NavigationType type,
|
QWebEnginePage::NavigationType type,
|
||||||
bool isMainFrame) override;
|
bool isMainFrame) override;
|
||||||
@@ -34,6 +48,9 @@ private:
|
|||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void featurePermissionRequested(const QUrl &securityOrigin,
|
void featurePermissionRequested(const QUrl &securityOrigin,
|
||||||
QWebEnginePage::Feature feature);
|
QWebEnginePage::Feature feature);
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void getUserStyles(QString url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Will immediately get destroyed again but is needed for navigation to
|
// Will immediately get destroyed again but is needed for navigation to
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ Q_LOGGING_CATEGORY(discordLog, "discord");
|
|||||||
Q_LOGGING_CATEGORY(userscriptLog, "userscript");
|
Q_LOGGING_CATEGORY(userscriptLog, "userscript");
|
||||||
Q_LOGGING_CATEGORY(virtmicLog, "virtmic");
|
Q_LOGGING_CATEGORY(virtmicLog, "virtmic");
|
||||||
Q_LOGGING_CATEGORY(shortcutLog, "shortcut");
|
Q_LOGGING_CATEGORY(shortcutLog, "shortcut");
|
||||||
|
Q_LOGGING_CATEGORY(userstylesLog, "userstyles");
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ Q_DECLARE_LOGGING_CATEGORY(discordLog);
|
|||||||
Q_DECLARE_LOGGING_CATEGORY(userscriptLog);
|
Q_DECLARE_LOGGING_CATEGORY(userscriptLog);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(virtmicLog);
|
Q_DECLARE_LOGGING_CATEGORY(virtmicLog);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(shortcutLog);
|
Q_DECLARE_LOGGING_CATEGORY(shortcutLog);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(userstylesLog);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include "dependencycheck.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "virtmic.h"
|
#include "virtmic.h"
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ int main(int argc, char *argv[]) {
|
|||||||
"--remote-debugging-port=9222 " +
|
"--remote-debugging-port=9222 " +
|
||||||
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
|
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
|
||||||
|
|
||||||
|
checkDependencies();
|
||||||
|
|
||||||
MainWindow w(parser.isSet(notifySendOption));
|
MainWindow w(parser.isSet(notifySendOption));
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,7 @@
|
|||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QWebEngineNotification>
|
#include <QWebEngineFullScreenRequest>
|
||||||
#include <QWebEngineProfile>
|
|
||||||
#include <QWebEngineScript>
|
|
||||||
#include <QWebEngineScriptCollection>
|
|
||||||
#include <QWebEngineSettings>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
MainWindow *MainWindow::m_instance = nullptr;
|
MainWindow *MainWindow::m_instance = nullptr;
|
||||||
@@ -29,12 +25,18 @@ MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
|
|||||||
: QMainWindow(parent) {
|
: QMainWindow(parent) {
|
||||||
assert(MainWindow::m_instance == nullptr);
|
assert(MainWindow::m_instance == nullptr);
|
||||||
MainWindow::m_instance = this;
|
MainWindow::m_instance = this;
|
||||||
m_useNotifySend = useNotifySend;
|
|
||||||
setupSettings();
|
setupSettings();
|
||||||
setupWebView();
|
m_settings->setValue("useNotifySend", useNotifySend);
|
||||||
|
m_centralWidget = new CentralWidget(this);
|
||||||
|
setCentralWidget(m_centralWidget);
|
||||||
setupTrayIcon();
|
setupTrayIcon();
|
||||||
resize(1000, 700);
|
setMinimumSize(800, 300);
|
||||||
showMaximized();
|
if (m_settings->contains("geometry")) {
|
||||||
|
restoreGeometry(m_settings->value("geometry").toByteArray());
|
||||||
|
} else {
|
||||||
|
resize(1000, 700);
|
||||||
|
showMaximized();
|
||||||
|
}
|
||||||
if (m_settings->value("trayIcon", false).toBool() &&
|
if (m_settings->value("trayIcon", false).toBool() &&
|
||||||
m_settings->value("startHidden", false).toBool()) {
|
m_settings->value("startHidden", false).toBool()) {
|
||||||
hide();
|
hide();
|
||||||
@@ -42,49 +44,6 @@ MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setupWebView() {
|
|
||||||
auto page = new DiscordPage(this);
|
|
||||||
connect(page, &QWebEnginePage::fullScreenRequested, this,
|
|
||||||
&MainWindow::fullScreenRequested);
|
|
||||||
|
|
||||||
m_webView = new QWebEngineView(this);
|
|
||||||
m_webView->setPage(page);
|
|
||||||
|
|
||||||
if (m_useKF5Notifications || m_useNotifySend)
|
|
||||||
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
|
|
||||||
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
|
|
||||||
if (m_useNotifySend) {
|
|
||||||
auto title = notificationInfo->title();
|
|
||||||
auto message = notificationInfo->message();
|
|
||||||
auto image_path =
|
|
||||||
QString("/tmp/discord-screenaudio-%1.png").arg(title);
|
|
||||||
notificationInfo->icon().save(image_path);
|
|
||||||
QProcess::execute("notify-send",
|
|
||||||
{"--icon", image_path, "--app-name",
|
|
||||||
"discord-screenaudio", title, message});
|
|
||||||
} else if (m_useKF5Notifications) {
|
|
||||||
#ifdef KNOTIFICATIONS
|
|
||||||
KNotification *notification =
|
|
||||||
new KNotification("discordNotification");
|
|
||||||
notification->setTitle(notificationInfo->title());
|
|
||||||
notification->setText(notificationInfo->message());
|
|
||||||
notification->setPixmap(
|
|
||||||
QPixmap::fromImage(notificationInfo->icon()));
|
|
||||||
notification->setDefaultAction("View");
|
|
||||||
connect(notification, &KNotification::defaultActivated,
|
|
||||||
[&, notificationInfo = std::move(notificationInfo)]() {
|
|
||||||
notificationInfo->click();
|
|
||||||
show();
|
|
||||||
activateWindow();
|
|
||||||
});
|
|
||||||
notification->sendEvent();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setCentralWidget(m_webView);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::fullScreenRequested(
|
void MainWindow::fullScreenRequested(
|
||||||
QWebEngineFullScreenRequest fullScreenRequest) {
|
QWebEngineFullScreenRequest fullScreenRequest) {
|
||||||
fullScreenRequest.accept();
|
fullScreenRequest.accept();
|
||||||
@@ -141,7 +100,8 @@ void MainWindow::cleanTrayIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setupSettings() {
|
void MainWindow::setupSettings() {
|
||||||
m_settings = new QSettings("maltejur", "discord-screenaudio", this);
|
m_settings =
|
||||||
|
new QSettings("discord-screenaudio", "discord-screenaudio", this);
|
||||||
m_settings->beginGroup("settings");
|
m_settings->beginGroup("settings");
|
||||||
m_settings->endGroup();
|
m_settings->endGroup();
|
||||||
}
|
}
|
||||||
@@ -160,8 +120,14 @@ void MainWindow::setTrayIcon(bool enabled) {
|
|||||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||||
if (m_settings->value("trayIcon", false).toBool()) {
|
if (m_settings->value("trayIcon", false).toBool()) {
|
||||||
hide();
|
hide();
|
||||||
} else
|
} else {
|
||||||
|
m_settings->setValue("geometry", saveGeometry());
|
||||||
QApplication::quit();
|
QApplication::quit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow *MainWindow::instance() { return m_instance; }
|
MainWindow *MainWindow::instance() { return m_instance; }
|
||||||
|
|
||||||
|
CentralWidget *MainWindow::centralWidget() {
|
||||||
|
return instance()->m_centralWidget;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "discordpage.h"
|
#include "centralwidget.h"
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
@@ -8,10 +8,8 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QSystemTrayIcon>
|
#include <QSystemTrayIcon>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QWebEnginePage>
|
|
||||||
#include <QWebEngineProfile>
|
|
||||||
#include <QWebEngineView>
|
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -20,31 +18,22 @@ public:
|
|||||||
explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
|
explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
|
||||||
static MainWindow *instance();
|
static MainWindow *instance();
|
||||||
QSettings *settings() const;
|
QSettings *settings() const;
|
||||||
|
static CentralWidget *centralWidget();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupWebView();
|
|
||||||
void setupTrayIcon();
|
void setupTrayIcon();
|
||||||
void cleanTrayIcon();
|
void cleanTrayIcon();
|
||||||
void setupSettings();
|
void setupSettings();
|
||||||
QWebEngineView *m_webView;
|
|
||||||
QWebEngineProfile *prepareProfile();
|
QWebEngineProfile *prepareProfile();
|
||||||
DiscordPage *m_discordPage;
|
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
QSystemTrayIcon *m_trayIcon = nullptr;
|
QSystemTrayIcon *m_trayIcon = nullptr;
|
||||||
QMenu *m_trayIconMenu;
|
QMenu *m_trayIconMenu;
|
||||||
QSettings *m_settings;
|
QSettings *m_settings;
|
||||||
bool m_wasMaximized;
|
bool m_wasMaximized;
|
||||||
static MainWindow *m_instance;
|
static MainWindow *m_instance;
|
||||||
bool m_useNotifySend;
|
CentralWidget *m_centralWidget;
|
||||||
#ifdef KNOTIFICATIONS
|
|
||||||
bool m_useKF5Notifications = true;
|
|
||||||
#else
|
|
||||||
bool m_useKF5Notifications = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void setTrayIcon(bool enabled);
|
void setTrayIcon(bool enabled);
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
|
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#ifdef KXMLGUI
|
#ifdef KXMLGUI
|
||||||
@@ -58,7 +62,7 @@ void UserScript::setupShortcutsDialog() {
|
|||||||
auto toggleDeafenAction = new QAction(this);
|
auto toggleDeafenAction = new QAction(this);
|
||||||
toggleDeafenAction->setText("Toggle Deafen");
|
toggleDeafenAction->setText("Toggle Deafen");
|
||||||
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
|
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
|
||||||
connect(toggleMuteAction, &QAction::triggered, this,
|
connect(toggleDeafenAction, &QAction::triggered, this,
|
||||||
&UserScript::deafenToggled);
|
&UserScript::deafenToggled);
|
||||||
|
|
||||||
m_actionCollection = new KActionCollection(this);
|
m_actionCollection = new KActionCollection(this);
|
||||||
@@ -162,4 +166,15 @@ void UserScript::showStreamDialog() {
|
|||||||
else
|
else
|
||||||
m_streamDialog->activateWindow();
|
m_streamDialog->activateWindow();
|
||||||
m_streamDialog->updateTargets();
|
m_streamDialog->updateTargets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserScript::showThemeDialog() {
|
||||||
|
auto url = QInputDialog::getText(MainWindow::instance(), "Theme Installation",
|
||||||
|
"Please enter the URL of the Theme");
|
||||||
|
if (url != "")
|
||||||
|
emit shouldInstallUserStyles(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserScript::installUserStyles(QString url) {
|
||||||
|
emit shouldInstallUserStyles(url);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef KNOTIFICATIONS
|
||||||
|
#include <KNotification>
|
||||||
|
#endif
|
||||||
|
|
||||||
class UserScript : public QObject {
|
class UserScript : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -27,12 +31,17 @@ public:
|
|||||||
Q_PROPERTY(QString version READ version CONSTANT);
|
Q_PROPERTY(QString version READ version CONSTANT);
|
||||||
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT);
|
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT);
|
||||||
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT);
|
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT);
|
||||||
|
Q_PROPERTY(QString userstyles MEMBER m_userstyles NOTIFY userstylesChanged);
|
||||||
|
Q_PROPERTY(QString loadingMessage MEMBER m_loadingMessage NOTIFY
|
||||||
|
loadingMessageChanged);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QProcess m_virtmicProcess;
|
QProcess m_virtmicProcess;
|
||||||
StreamDialog *m_streamDialog;
|
StreamDialog *m_streamDialog;
|
||||||
bool m_kxmlgui = false;
|
bool m_kxmlgui = false;
|
||||||
bool m_kglobalaccel = false;
|
bool m_kglobalaccel = false;
|
||||||
|
QString m_userstyles;
|
||||||
|
QString m_loadingMessage;
|
||||||
#ifdef KXMLGUI
|
#ifdef KXMLGUI
|
||||||
KHelpMenu *m_helpMenu;
|
KHelpMenu *m_helpMenu;
|
||||||
#ifdef KGLOBALACCEL
|
#ifdef KGLOBALACCEL
|
||||||
@@ -49,6 +58,9 @@ Q_SIGNALS:
|
|||||||
void muteToggled();
|
void muteToggled();
|
||||||
void deafenToggled();
|
void deafenToggled();
|
||||||
void streamStarted(bool video, int width, int height, int frameRate);
|
void streamStarted(bool video, int width, int height, int frameRate);
|
||||||
|
void userstylesChanged();
|
||||||
|
void loadingMessageChanged(QString message);
|
||||||
|
void shouldInstallUserStyles(QString url);
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void log(QString message);
|
void log(QString message);
|
||||||
@@ -62,6 +74,8 @@ public Q_SLOTS:
|
|||||||
void showStreamDialog();
|
void showStreamDialog();
|
||||||
void stopVirtmic();
|
void stopVirtmic();
|
||||||
void startVirtmic(QString target);
|
void startVirtmic(QString target);
|
||||||
|
void showThemeDialog();
|
||||||
|
void installUserStyles(QString url);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void startStream(bool video, bool audio, int width, int height, int frameRate,
|
void startStream(bool video, bool audio, int width, int height, int frameRate,
|
||||||
|
|||||||
Reference in New Issue
Block a user