-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEA093du9eDGE9L3k00p+9X5PAz8MFAmPsN6oACgkQ0p+9X5PA
 z8O8pg//UBo01WAv4NwK17gLIMMjwbXIcWCcC46RaJKo0gBSJy9wFvHCQfR3qLb4
 AJDlfnbSkkJ5w3xnEruUwmgbTkBOWnxYUX/Uaq2jI7JfxYWi1AdMINg0E7GirLqs
 T1AraC5bjaIlrfohgspi9BYw8g2fBE/doQKrUC5a4vTwPYmjZZmCOkwILPTKx8Ig
 L5CmBQPnpOMtfOvwyM7h3I4FT32M2WwW79gW9XYKNf3kiXDRqqTQODMzU0iNjHq3
 STDu3lrCgpdEyNuBsZQilfCWTPCVHTggYQGv0Brv+YPRE/ELzcfFuqqQcpvZC843
 hobJIuCNfCfJN7Bu6gc2RUfNZkDQUj7Ar62j+J7PS5CVfQjpwomfeDeX94vaq8hK
 JWret1lHzabbBmeEcX41wq/WcHMGfFzk30PlLK3f1EQqdu/QUvprXV3pQDWkrqdn
 gtamTz3TIxUBWd2dbC8oaOQLB9YKPLCJy+zbPNObNMII5PU3nmHSFMafTefjjiwA
 x1t5hwxwM6ND+YsDU3XTvMVLo87enQgnNhsfQyrIRPqaIRerB02w2j++b2SCluX1
 npsJhVK/ND9aih+x/dopMXDqJTVJhLqH31KOaRz2l7KTAubY/c8gVSnVBKDTp4ez
 /RuOjBys5qXNKU/RRn9cA09xuXCkDZK7WBtHpU6vqKmEuP18ND4=
 =vRVO
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEA093du9eDGE9L3k00p+9X5PAz8MFAmPw5voACgkQ0p+9X5PA
 z8PoCg//T+mXN0/wp0/nh8T44nptqBDy28RJzzWrbxyNLx5uazU0Nq97jToiY3QI
 3XnjtqN1pv+6DQeBKpJcHX3jsUQOXXqDWeVR4gsqjBe3w54mgRTUmzFUcnUsERud
 D/HI04vV2Z2+ky5opS3eQFhy9sVPwGvEMZRTsSNMkGYESGzvOzl3SXCDW4ftNm8Q
 9Qu11BlxYxu4NNKBtvbDPVICcXk2YRdjBBQ8dN66nyrHLa3AO/tbOP9mh8mLJuDB
 WjEaG29+Xpsx5l9woLg0JakGyf2HkYuHG/G/oEUknsCJ7/MUb0iknwFWy9BdaiO+
 Dnh4ABoK0PGDpzHAR5OUomyvAfOd8AOXcFaOztT/NJ6Tj1RNZgbPKbsS+wJxYzGN
 x8xsUXhGIY/y9EqEcKolRh3JKMihtvdVjfOaeRFse0juNErewsurSx+oxcSS8jj/
 ULABMR/SpDp+h0j9HGexqNnI7htGe2egabRIMUfqfN/kKO4i5onvTbq7gpIaak6B
 W3BAzPnAes81VKgcLIcEqrPBSTuAxb1L3tNeKU3dHMeFQz2P6p3CFx/UPObIdwzv
 GdX6hzQKIg/IGh+5i31Heyh6wB1e5FhEUKxqDrE1wVOPVDkP6bHlZEW0lI2feSYM
 xjk1/w5onEOKiXvXkgC64f7wZY/VCXOaa3Ptp2hZSLuypDgNxdY=
 =D7Pe
 -----END PGP SIGNATURE-----

Merge tag 'v1.7.0' into vencord

v1.7.0
This commit is contained in:
Malte Jürgens 2023-02-18 15:55:54 +01:00
commit e3d87e8202
16 changed files with 992 additions and 512 deletions

View File

@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
string(TIMESTAMP TIMESTAMP %s) string(TIMESTAMP TIMESTAMP %s)
# set(CMAKE_AUTOUIC ON) # set(CMAKE_AUTOUIC ON)
find_package(Qt5 CONFIG REQUIRED COMPONENTS find_package(Qt5 COMPONENTS Widgets)
Widgets if (Qt5_FOUND)
WebEngineWidgets find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
)
find_package(KF5Notifications) find_package(KF5Notifications)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
add_definitions( -DKNOTIFICATIONS ) add_definitions( -DKNOTIFICATIONS )
endif() endif()
find_package(KF5XmlGui) find_package(KF5XmlGui)
if(KF5XmlGui_FOUND) if(KF5XmlGui_FOUND)
add_definitions( -DKXMLGUI ) add_definitions( -DKXMLGUI )
endif() endif()
find_package(KF5GlobalAccel) find_package(KF5GlobalAccel)
if(KF5GlobalAccel_FOUND) if(KF5GlobalAccel_FOUND)
add_definitions( -DKGLOBALACCEL ) add_definitions( -DKGLOBALACCEL )
endif()
else()
message(WARNING "Qt 5 was not found on your system and Qt 6 will be used. You will not be able to use any features using KDE Frameworks.")
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
endif() endif()
set(discord-screenaudio_SRC set(discord-screenaudio_SRC
@ -40,7 +43,7 @@ set(discord-screenaudio_SRC
src/discordpage.cpp src/discordpage.cpp
src/streamdialog.cpp src/streamdialog.cpp
src/log.cpp src/log.cpp
src/webclass.cpp src/userscript.cpp
resources.qrc resources.qrc
) )
@ -67,7 +70,7 @@ add_subdirectory(submodules/rohrkabel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC}) add_executable(discord-screenaudio ${discord-screenaudio_SRC})
target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel) target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
target_link_libraries(discord-screenaudio KF5::Notifications) target_link_libraries(discord-screenaudio KF5::Notifications)

View File

@ -11,7 +11,7 @@ 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-it-works)).
![Screenshot_20220925_112945](https://user-images.githubusercontent.com/48161361/192137080-33466cf7-8c56-4373-90c6-01ea74b6fb83.png) ![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png)
The purpose of this project is **not** to provide an alternative to the original The purpose of this project is **not** to provide an alternative to the original
Discord client. Rather, it should be used in addition to the original client in Discord client. Rather, it should be used in addition to the original client in
@ -50,6 +50,8 @@ You have multiple options:
### Requirements ### Requirements
- Basic building tools - Basic building tools
- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu
20/21)
- CMake - CMake
- Qt5 and QtWebEngine - Qt5 and QtWebEngine
- **PipeWire** (it currently doesn't work with PulseAudio) - **PipeWire** (it currently doesn't work with PulseAudio)
@ -57,7 +59,7 @@ You have multiple options:
- _Kf5Notifications (optional, for better notifications)_ - _Kf5Notifications (optional, for better notifications)_
- _KXMLGui and KGlobalAccel (optional, for keybinds)_ - _KXMLGui and KGlobalAccel (optional, for keybinds)_
On Debian: With apt:
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git` `apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git`
### Building ### Building
@ -82,7 +84,9 @@ And then to optionally install it, run:
sudo cmake --install build sudo cmake --install build
``` ```
## How it works ## FAQ
### How does this work?
This whole project is based on This whole project is based on
[this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) [this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
@ -91,6 +95,21 @@ Discord. Basically: a virtual microphone is created which captures the
application audio, and this microphone is then fed to the Discord stream by application audio, and this microphone is then fed to the Discord stream by
intercepting a API call of Discord. intercepting a API call of Discord.
### Drag and drop doesn't work in the Flatpak
This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has
the same problem. If you still want to use drag and drop, you can disable most
of Flatpak's sandboxing by installing
[Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and
allowing access to "All system files" under the "Filesystem" section.
### Is there any way to add custom CSS / a theme?
Yes, you can add all your styles into
`~/.config/discord-screenaudio/userstyles.css`. 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
Copyright (C) 2022 Malte Jürgens Copyright (C) 2022 Malte Jürgens

View File

@ -4,6 +4,7 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>discord-screenaudio</name> <name>discord-screenaudio</name>
<developer_name>Malte Jürgens</developer_name>
<releases> <releases>
<release version="${DISCORD_SCEENAUDIO_VERSION_FULL}" timestamp="${TIMESTAMP}" /> <release version="${DISCORD_SCEENAUDIO_VERSION_FULL}" timestamp="${TIMESTAMP}" />
</releases> </releases>

View File

@ -1,5 +1,3 @@
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia = navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia; navigator.mediaDevices.getDisplayMedia;
@ -16,16 +14,16 @@ const getAudioDevice = async (nameOfAudioDevice) => {
let devices = await navigator.mediaDevices.enumerateDevices(); let devices = await navigator.mediaDevices.enumerateDevices();
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
if (!audioDevice) if (!audioDevice)
console.log( userscript.log(
`dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms` `Did not find '${nameOfAudioDevice}', trying again in 100ms`
); );
await sleep(100); await sleep(100);
} }
console.log(`dsa: Found '${nameOfAudioDevice}'`); userscript.log(`Found '${nameOfAudioDevice}'`);
return audioDevice; return audioDevice;
}; };
function setGetDisplayMedia(overrideArgs = undefined) { function setGetDisplayMedia(video = true, overrideArgs = undefined) {
const getDisplayMedia = async (...args) => { const getDisplayMedia = async (...args) => {
var id; var id;
try { try {
@ -63,6 +61,7 @@ function setGetDisplayMedia(overrideArgs = undefined) {
: args || [{ video: true, audio: true }]) : args || [{ video: true, audio: true }])
); );
gdm.addTrack(track); gdm.addTrack(track);
if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm; return gdm;
}; };
navigator.mediaDevices.getDisplayMedia = getDisplayMedia; navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@ -70,17 +69,100 @@ function setGetDisplayMedia(overrideArgs = undefined) {
setGetDisplayMedia(); setGetDisplayMedia();
let userscript;
let muteBtn;
let deafenBtn;
let streamStartBtn;
let streamStartBtnInitialDisplay;
let streamStartBtnClone;
let resolutionString;
const clonedElements = []; const clonedElements = [];
const hiddenElements = []; const hiddenElements = [];
let wasStreamActive = false; let wasStreamActive = false;
setInterval(() => { function createButton(text, onClick) {
const button = document.createElement("button");
button.style.marginBottom = "20px";
button.classList =
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
}
function createSwitch(text, enabled, onClick) {
const container = document.createElement("div");
container.style.marginBottom = "20px";
container.className = "labelRow-NnoUIp";
const label = document.createElement("label");
label.innerText = text;
label.className = "title-2yADjX";
container.appendChild(label);
const svg = document.createElement("div");
container.appendChild(svg);
function setSvgDisabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`;
}
function setSvgEnabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`;
}
function updateSvg() {
if (enabled) setSvgEnabled();
else setSvgDisabled();
}
container.addEventListener("click", () => {
enabled = !enabled;
updateSvg();
onClick(enabled);
});
updateSvg();
return container;
}
// Fix for broken discord notifications after restart
// (https://github.com/maltejur/discord-screenaudio/issues/17)
Notification.requestPermission();
setTimeout(() => {
new QWebChannel(qt.webChannelTransport, (channel) => {
userscript = channel.objects.userscript;
main();
});
});
function main() {
userscript.muteToggled.connect(() => {
muteBtn && muteBtn.click();
});
userscript.deafenToggled.connect(() => {
deafenBtn && deafenBtn.click();
});
userscript.streamStarted.connect((video, width, height, frameRate) => {
resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only";
setGetDisplayMedia(video, {
audio: true,
video: { width, height, frameRate },
});
streamStartBtn.click();
streamStartBtn.style.display = streamStartBtnInitialDisplay;
streamStartBtnClone.remove();
});
setInterval(async () => {
const streamActive = const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
.length > 0; .length > 0;
if (!streamActive && wasStreamActive) if (!streamActive && wasStreamActive) userscript.stopVirtmic();
console.log("!discord-screenaudio-stream-stopped");
wasStreamActive = streamActive; wasStreamActive = streamActive;
if (streamActive) { if (streamActive) {
@ -95,59 +177,54 @@ setInterval(() => {
hiddenElements.length = 0; hiddenElements.length = 0;
} else { } else {
for (const el of [ for (const el of [
document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1], document.getElementsByClassName("actionButtons-2vEOUh")?.[0]
?.children[1],
document.querySelector( document.querySelector(
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
), ),
]) { ]) {
if (!el) continue; if (!el) continue;
if (el.classList.contains("discord-screenaudio-cloned")) continue; if (el.classList.contains("discord-screenaudio-cloned")) continue;
el.classList.add("discord-screenaudio-cloned"); streamStartBtn = el;
elClone = el.cloneNode(true); streamStartBtn.classList.add("discord-screenaudio-cloned");
elClone.title = "Share Your Screen with Audio";
elClone.addEventListener("click", () => { streamStartBtnClone = streamStartBtn.cloneNode(true);
console.log("!discord-screenaudio-start-stream"); streamStartBtnClone.title = "Share Your Screen with Audio";
streamStartBtnClone.addEventListener("click", () => {
userscript.showStreamDialog();
}); });
const initialDisplay = el.style.display; streamStartBtnInitialDisplay = streamStartBtn.style.display;
window.discordScreenaudioStartStream = (width, height, frameRate) => { streamStartBtn.style.display = "none";
window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`; streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el);
setGetDisplayMedia({
audio: true,
video: { width, height, frameRate },
});
el.click();
el.style.display = initialDisplay;
elClone.remove();
};
el.style.display = "none"; clonedElements.push(streamStartBtnClone);
el.parentNode.insertBefore(elClone, el); hiddenElements.push(streamStartBtn);
clonedElements.push(elClone);
hiddenElements.push(el);
} }
} }
// Add about text in settings // Add about text in settings
if ( if (
document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0 document.getElementsByClassName("dirscordScreenaudioAboutText").length ==
0
) { ) {
for (const el of document.getElementsByClassName("info-3pQQBb")) { for (const el of document.getElementsByClassName("info-3pQQBb")) {
let aboutEl; let aboutEl;
if (window.discordScreenaudioKXMLGUI) { if (userscript.kxmlgui) {
aboutEl = document.createElement("a"); aboutEl = document.createElement("a");
aboutEl.addEventListener("click", () => { aboutEl.addEventListener("click", () => {
console.log("!discord-screenaudio-about"); userscript.showHelpMenu();
}); });
} else { } else {
aboutEl = document.createElement("div"); aboutEl = document.createElement("div");
} }
aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`; aboutEl.innerText = `discord-screenaudio ${userscript.version}`;
aboutEl.style.fontSize = "12px"; aboutEl.style.fontSize = "12px";
aboutEl.style.color = "var(--text-muted)"; aboutEl.style.color = "var(--text-muted)";
aboutEl.style.textTransform = "none"; aboutEl.style.textTransform = "none";
aboutEl.style.display = "inline-block";
aboutEl.style.width = "100%";
aboutEl.classList.add("dirscordScreenaudioAboutText"); aboutEl.classList.add("dirscordScreenaudioAboutText");
aboutEl.style.cursor = "pointer"; aboutEl.style.cursor = "pointer";
el.appendChild(aboutEl); el.appendChild(aboutEl);
@ -163,7 +240,7 @@ setInterval(() => {
document document
.getElementById("keybinds-tab") .getElementById("keybinds-tab")
?.getElementsByClassName( ?.getElementsByClassName(
"container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o" "container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
).length ).length
) { ) {
const el = document const el = document
@ -171,36 +248,96 @@ setInterval(() => {
.getElementsByClassName("children-1xdcWE")[0]; .getElementsByClassName("children-1xdcWE")[0];
const div = document.createElement("div"); const div = document.createElement("div");
div.style.marginBottom = "50px"; div.style.marginBottom = "50px";
const button = document.createElement("button"); div.appendChild(
button.classList = createButton("Edit Global Keybinds", () => {
"button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F"; userscript.showShortcutsDialog();
button.innerText = "Edit Global Keybinds"; })
button.addEventListener("click", () => { );
console.log("!discord-screenaudio-keybinds");
});
div.appendChild(button);
el.innerHTML = ""; el.innerHTML = "";
el.appendChild(div); el.appendChild(div);
} }
const muteBtn = document.getElementsByClassName( const buttonContainer =
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" document.getElementsByClassName("container-YkUktl")[0];
)[0]; if (!buttonContainer) {
window.discordScreenaudioToggleMute = () => muteBtn.click(); userscript.log(
const deafenBtn = document.getElementsByClassName( "Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" );
)[1]; }
window.discordScreenaudioToggleDeafen = () => deafenBtn.click();
if (window.discordScreenaudioResolutionString) { muteBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[0]
: null;
deafenBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[1]
: null;
if (resolutionString) {
for (const el of document.getElementsByClassName( for (const el of document.getElementsByClassName(
"qualityIndicator-39wQDy" "qualityIndicator-39wQDy"
)) { )) {
el.innerHTML = window.discordScreenaudioResolutionString; el.innerHTML = resolutionString;
} }
} }
}, 500);
// Fix for broken discord notifications after restart const accountTab = document.getElementById("my-account-tab");
// (https://github.com/maltejur/discord-screenaudio/issues/17) if (accountTab) {
Notification.requestPermission(); const discordScreenaudioSettings = document.getElementById(
"discord-screenaudio-settings"
);
if (!discordScreenaudioSettings) {
const firstDivider = accountTab.getElementsByClassName(
"divider-3nqZNm marginTop40-Q4o1tS"
)[0];
if (firstDivider) {
const section = document.createElement("div");
section.className = "marginTop40-Q4o1tS";
section.id = "discord-screenaudio-settings";
const title = document.createElement("h2");
title.className =
"h1-3iMExa title-lXcL8p defaultColor-3Olr-9 defaultMarginh1-1UYutH";
title.innerText = "discord-screenaudio";
section.appendChild(title);
section.appendChild(
createButton("Edit Global Keybinds", () => {
userscript.showShortcutsDialog();
})
);
section.appendChild(
createSwitch(
"Move discord-screenaudio to the system tray instead of closing",
await userscript.getBoolPref("trayIcon", false),
(enabled) => {
userscript.setTrayIcon(enabled);
}
)
);
section.appendChild(
createSwitch(
"Start discord-screenaudio hidden to tray",
await userscript.getPref("startHidden", false),
(hidden) => {
userscript.setPref("startHidden", hidden);
}
)
);
const divider = document.createElement("div");
divider.className = "divider-3nqZNm marginTop40-Q4o1tS";
firstDivider.after(section);
section.after(divider);
}
}
}
}, 500);
}

View File

@ -3,22 +3,10 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <QTimer> #include <QTimer>
@ -29,16 +17,40 @@
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#202225")); setBackgroundColor(QColor("#202225"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this, connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested); &DiscordPage::featurePermissionRequested);
connect(this, &QWebEnginePage::loadStarted, [=]() { setupPermissions();
runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
});
injectFile(&DiscordPage::injectScript, "qwebchannel.js",
":/qtwebchannel/qwebchannel.js");
setUrl(QUrl("https://discord.com/app"));
setWebChannel(new QWebChannel(this));
webChannel()->registerObject("userscript", &m_userScript);
injectFile(&DiscordPage::injectScript, "userscript.js",
":/assets/userscript.js");
injectFile(&DiscordPage::injectScript, "userscript.js",
":/assets/userscript.js");
QFile vencord(":/assets/vencord/vencord.js");
if (!vencord.open(QIODevice::ReadOnly))
qFatal("Failed to load vencord source with error: %s",
vencord.errorString().toLatin1().constData());
injectScript(
"vencord.js",
QString("window.discordScreenaudioVencordSettings = `%1`; %2")
.arg(m_userScript.vencordSend("VencordGetSettings", {}).toString(),
vencord.readAll()));
vencord.close();
setupUserStyles();
}
void DiscordPage::setupPermissions() {
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@ -50,86 +62,19 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
false); false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
injectScriptFile("qwebchannel.js", ":/qtwebchannel/qwebchannel.js");
setUrl(QUrl("https://discord.com/app"));
setWebChannel(new QWebChannel(this));
webChannel()->registerObject("webclass", &m_webClass);
injectScriptFile("userscript.js", ":/assets/userscript.js");
QFile vencord(":/assets/vencord/vencord.js");
if (!vencord.open(QIODevice::ReadOnly))
qFatal("Failed to load vencord source with error: %s",
vencord.errorString().toLatin1().constData());
injectScriptText(
"vencord.js",
QString("window.discordScreenaudioVencordSettings = `%1`; %2")
.arg(m_webClass.vencordSend("VencordGetSettings", {}).toString(),
vencord.readAll()));
vencord.close();
injectScriptText("version.js",
QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
#ifdef KXMLGUI
injectScriptText("xmlgui.js", "window.discordScreenaudioKXMLGUI = true;");
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
m_helpMenu = new KHelpMenu(parent, aboutData);
#ifdef KGLOBALACCEL
injectScriptText("kglobalaccel.js",
"window.discordScreenaudioKGLOBALACCEL = true;");
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&DiscordPage::toggleMute);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleDeafenAction, &QAction::triggered, this,
&DiscordPage::toggleDeafen);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
} }
void DiscordPage::injectScriptText( void DiscordPage::setupUserStyles() {
QString file =
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
"/userstyles.css";
if (QFileInfo(file).exists()) {
qDebug(mainLog) << "Found userstyles:" << file;
injectFile(&DiscordPage::injectStylesheet, "userstyles.js", file);
}
}
void DiscordPage::injectScript(
QString name, QString content, QString name, QString content,
QWebEngineScript::InjectionPoint injectionPoint) { QWebEngineScript::InjectionPoint injectionPoint) {
qDebug(mainLog) << "Injecting " << name; qDebug(mainLog) << "Injecting " << name;
@ -145,16 +90,31 @@ void DiscordPage::injectScriptText(
scripts().insert(script); scripts().insert(script);
} }
void DiscordPage::injectScriptFile( void DiscordPage::injectScript(QString name, QString content) {
QString name, QString source, injectScript(name, content, QWebEngineScript::DocumentCreation);
QWebEngineScript::InjectionPoint injectionPoint) { }
void DiscordPage::injectStylesheet(QString name, QString content) {
auto script = QString(R"(const stylesheet = document.createElement("style");
stylesheet.type = "text/css";
stylesheet.id = "%1";
stylesheet.innerText = `%2`;
document.head.appendChild(stylesheet);
)")
.arg(name)
.arg(content);
injectScript(name, script, QWebEngineScript::DocumentReady);
}
void DiscordPage::injectFile(void (DiscordPage::*inject)(QString, QString),
QString name, QString source) {
QFile file(source); QFile file(source);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(), qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
file.errorString().toLatin1().constData()); file.errorString().toLatin1().constData());
} else { } else {
injectScriptText(name, file.readAll(), injectionPoint); (this->*inject)(name, file.readAll());
} }
} }
@ -165,11 +125,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::PermissionGrantedByUser); QWebEnginePage::PermissionGrantedByUser);
if (feature == QWebEnginePage::Feature::MediaAudioCapture) { if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
if (m_virtmicProcess.state() == QProcess::NotRunning) { if (!m_userScript.isVirtmicRunning()) {
qDebug(virtmicLog) << "Starting Virtmic with no target to make sure " qDebug(virtmicLog) << "Starting Virtmic with no target to make sure "
"Discord can find all the audio devices"; "Discord can find all the audio devices";
m_virtmicProcess.start(QApplication::arguments()[0], m_userScript.startVirtmic("None");
{"--virtmic", "None"});
} }
} }
} }
@ -196,78 +155,81 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
return new ExternalPage; return new ExternalPage;
} }
void DiscordPage::stopVirtmic() { const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"},
if (m_virtmicProcess.state() == QProcess::Running) { {"red", "31"},
qDebug(virtmicLog) << "Stopping Virtmic"; {"green", "32"},
m_virtmicProcess.kill(); {"yellow", "33"},
m_virtmicProcess.waitForFinished(); {"blue", "34"},
} {"magenta", "35"},
} {"cyan", "36"},
{"white", "37"},
void DiscordPage::startVirtmic(QString target) { {"gray", "90"},
if (target != "None") { {"bright-red", "91"},
qDebug(virtmicLog) << "Starting Virtmic with target" << target; {"bright-green", "92"},
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target}); {"bright-yellow", "93"},
} {"bright-blue", "94"},
} {"bright-magenta", "95"},
{"bright-cyan", "96"},
{"bright-white", "97"},
{"orange", "38;5;208"},
{"pink", "38;5;205"},
{"brown", "38;5;94"},
{"light-gray", "38;5;251"},
{"dark-gray", "38;5;239"},
{"light-red", "38;5;203"},
{"light-green", "38;5;83"},
{"light-yellow", "38;5;227"},
{"light-blue", "38;5;75"},
{"light-magenta", "38;5;207"},
{"light-cyan", "38;5;87"},
{"turquoise", "38;5;80"},
{"violet", "38;5;92"},
{"purple", "38;5;127"},
{"lavender", "38;5;183"},
{"maroon", "38;5;124"},
{"beige", "38;5;230"},
{"olive", "38;5;142"},
{"indigo", "38;5;54"},
{"teal", "38;5;30"},
{"gold", "38;5;220"},
{"silver", "38;5;7"},
{"navy", "38;5;17"},
{"steel", "38;5;188"},
{"salmon", "38;5;173"},
{"peach", "38;5;217"},
{"khaki", "38;5;179"},
{"coral", "38;5;209"},
{"crimson", "38;5;160"}};
void DiscordPage::javaScriptConsoleMessage( 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) {
if (message == "!discord-screenaudio-start-stream") { auto colorSegments = message.split("%c");
if (m_streamDialog.isHidden()) for (auto segment : colorSegments.mid(1)) {
m_streamDialog.setHidden(false); auto lines = segment.split("\n");
else QString ansi;
m_streamDialog.activateWindow(); uint endOfStyles = lines.length();
m_streamDialog.updateTargets(); for (size_t line = 1; line < lines.length(); line++) {
} else if (message == "!discord-screenaudio-stream-stopped") { if (!lines[line].endsWith(";")) {
stopVirtmic(); endOfStyles = line;
} else if (message == "!discord-screenaudio-about") { break;
#ifdef KXMLGUI }
m_helpMenu->aboutApplication(); if (lines[line] == "font-weight: bold;")
#endif ansi += "\033[1m";
} else if (message == "!discord-screenaudio-keybinds") { else if (lines[line].startsWith("color: ")) {
#ifdef KXMLGUI auto color = lines[line].mid(7).chopped(1);
#ifdef KGLOBALACCEL if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end())
m_shortcutsDialog->show(); ansi += "\033[" + cssAnsiColorMap[color] + "m";
#else }
QMessageBox::information(MainWindow::instance(), "discord-screenaudio", }
"Keybinds are not supported on this platform " qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
"(KGlobalAccel is not available).", ((lines.length() > endOfStyles)
QMessageBox::Ok); ? lines[endOfStyles].trimmed()
#endif : ""))
#else .toUtf8()
QMessageBox::information(MainWindow::instance(), "discord-screenaudio", .constData();
"Keybinds are not supported on this platform " for (auto line : lines.mid(endOfStyles + 1)) {
"(KXmlGui and KGlobalAccel are not available).", qDebug(discordLog) << line.toUtf8().constData();
QMessageBox::Ok); }
#endif
} else if (message.startsWith("dsa: ")) {
qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
} else {
qDebug(discordLog) << message;
} }
} }
void DiscordPage::startStream(QString target, uint width, uint height,
uint frameRate) {
stopVirtmic();
startVirtmic(target);
// Wait a bit for the virtmic to start
QTimer::singleShot(target == "None" ? 0 : 200, [=]() {
runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);")
.arg(width)
.arg(height)
.arg(frameRate));
});
}
void DiscordPage::toggleMute() {
qDebug(shortcutLog) << "Toggling mute";
runJavaScript("window.discordScreenaudioToggleMute();");
}
void DiscordPage::toggleDeafen() {
qDebug(shortcutLog) << "Toggling deafen";
runJavaScript("window.discordScreenaudioToggleDeafen();");
}

View File

@ -1,16 +1,9 @@
#pragma once #pragma once
#include "streamdialog.h" #include "streamdialog.h"
#include "userscript.h"
#include "virtmic.h" #include "virtmic.h"
#include "webclass.h"
#ifdef KXMLGUI
#include <KActionCollection>
#include <KHelpMenu>
#include <KShortcutsDialog>
#endif
#include <QProcess>
#include <QWebEngineFullScreenRequest> #include <QWebEngineFullScreenRequest>
#include <QWebEnginePage> #include <QWebEnginePage>
#include <QWebEngineScript> #include <QWebEngineScript>
@ -22,16 +15,9 @@ public:
explicit DiscordPage(QWidget *parent = nullptr); explicit DiscordPage(QWidget *parent = nullptr);
private: private:
StreamDialog m_streamDialog; UserScript m_userScript;
QProcess m_virtmicProcess; void setupPermissions();
WebClass m_webClass; void setupUserStyles();
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
bool acceptNavigationRequest(const QUrl &url, bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type, QWebEnginePage::NavigationType type,
bool isMainFrame) override; bool isMainFrame) override;
@ -40,21 +26,16 @@ private:
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber, const QString &message, int lineNumber,
const QString &sourceID) override; const QString &sourceID) override;
void injectScriptText(QString name, QString content, void injectScript(QString name, QString content,
QWebEngineScript::InjectionPoint injectionPoint = QWebEngineScript::InjectionPoint injectionPoint);
QWebEngineScript::DocumentCreation); void injectScript(QString name, QString content);
void injectScriptFile(QString name, QString source, void injectStylesheet(QString name, QString content);
QWebEngineScript::InjectionPoint injectionPoint = void injectFile(void (DiscordPage::*inject)(QString, QString), QString name,
QWebEngineScript::DocumentCreation); QString source);
void stopVirtmic();
void startVirtmic(QString target);
void toggleMute();
void toggleDeafen();
private Q_SLOTS: private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin, void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature); QWebEnginePage::Feature feature);
void startStream(QString target, uint width, uint height, uint frameRate);
}; };
// Will immediately get destroyed again but is needed for navigation to // Will immediately get destroyed again but is needed for navigation to

View File

@ -30,6 +30,9 @@ int main(int argc, char *argv[]) {
QCommandLineOption degubOption("remote-debugging", QCommandLineOption degubOption("remote-debugging",
"Open Chromium Remote Debugging on port 9222"); "Open Chromium Remote Debugging on port 9222");
parser.addOption(degubOption); parser.addOption(degubOption);
QCommandLineOption notifySendOption(
"notify-send", "Use notify-send instead of QT/KF5 notifications");
parser.addOption(notifySendOption);
parser.process(app); parser.process(app);
@ -46,7 +49,7 @@ int main(int argc, char *argv[]) {
"--remote-debugging-port=9222 " + "--remote-debugging-port=9222 " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
MainWindow w; MainWindow w(parser.isSet(notifySendOption));
w.show(); w.show();
return app.exec(); return app.exec();

View File

@ -14,6 +14,7 @@
#include <QPushButton> #include <QPushButton>
#include <QSpacerItem> #include <QSpacerItem>
#include <QThread> #include <QThread>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWebEngineNotification> #include <QWebEngineNotification>
#include <QWebEngineProfile> #include <QWebEngineProfile>
@ -24,12 +25,21 @@
MainWindow *MainWindow::m_instance = nullptr; MainWindow *MainWindow::m_instance = nullptr;
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { MainWindow::MainWindow(bool useNotifySend, QWidget *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();
setupWebView(); setupWebView();
setupTrayIcon();
resize(1000, 700); resize(1000, 700);
showMaximized(); showMaximized();
if (m_settings->value("trayIcon", false).toBool() &&
m_settings->value("startHidden", false).toBool()) {
hide();
QTimer::singleShot(0, [=]() { hide(); });
}
} }
void MainWindow::setupWebView() { void MainWindow::setupWebView() {
@ -40,22 +50,37 @@ void MainWindow::setupWebView() {
m_webView = new QWebEngineView(this); m_webView = new QWebEngineView(this);
m_webView->setPage(page); m_webView->setPage(page);
#ifdef KNOTIFICATIONS if (m_useKF5Notifications || m_useNotifySend)
QWebEngineProfile::defaultProfile()->setNotificationPresenter( QWebEngineProfile::defaultProfile()->setNotificationPresenter(
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) { [&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
KNotification *notification = new KNotification("discordNotification"); 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->setTitle(notificationInfo->title());
notification->setText(notificationInfo->message()); notification->setText(notificationInfo->message());
notification->setPixmap(QPixmap::fromImage(notificationInfo->icon())); notification->setPixmap(
QPixmap::fromImage(notificationInfo->icon()));
notification->setDefaultAction("View"); notification->setDefaultAction("View");
connect(notification, &KNotification::defaultActivated, connect(notification, &KNotification::defaultActivated,
[&, notificationInfo = std::move(notificationInfo)]() { [&, notificationInfo = std::move(notificationInfo)]() {
notificationInfo->click(); notificationInfo->click();
show();
activateWindow(); activateWindow();
}); });
notification->sendEvent(); notification->sendEvent();
});
#endif #endif
}
});
setCentralWidget(m_webView); setCentralWidget(m_webView);
} }
@ -71,6 +96,72 @@ void MainWindow::fullScreenRequested(
} }
} }
void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); } void MainWindow::setupTrayIcon() {
if (m_settings->value("trayIcon", false).toBool() == false ||
m_trayIcon != nullptr)
return;
auto aboutAction = new QAction(
"discord-screenaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this);
aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
aboutAction->setEnabled(false);
auto exitAction = new QAction("Exit", this);
connect(exitAction, &QAction::triggered, []() { QApplication::quit(); });
m_trayIconMenu = new QMenu(this);
m_trayIconMenu->addAction(aboutAction);
m_trayIconMenu->addAction(exitAction);
m_trayIcon = new QSystemTrayIcon(this);
m_trayIcon->setContextMenu(m_trayIconMenu);
m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
m_trayIcon->show();
connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) {
if (reason == QSystemTrayIcon::Trigger) {
if (isVisible()) {
hide();
} else {
show();
activateWindow();
}
}
});
}
void MainWindow::cleanTrayIcon() {
if (m_trayIcon == nullptr)
return;
m_trayIcon->hide();
m_trayIconMenu->deleteLater();
m_trayIcon->deleteLater();
m_trayIconMenu = nullptr;
m_trayIcon = nullptr;
}
void MainWindow::setupSettings() {
m_settings = new QSettings("maltejur", "discord-screenaudio", this);
m_settings->beginGroup("settings");
m_settings->endGroup();
}
QSettings *MainWindow::settings() const { return m_settings; }
void MainWindow::setTrayIcon(bool enabled) {
m_settings->setValue("trayIcon", enabled);
if (enabled) {
setupTrayIcon();
} else {
cleanTrayIcon();
}
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (m_settings->value("trayIcon", false).toBool()) {
hide();
} else
QApplication::quit();
}
MainWindow *MainWindow::instance() { return m_instance; } MainWindow *MainWindow::instance() { return m_instance; }

View File

@ -3,8 +3,11 @@
#include "discordpage.h" #include "discordpage.h"
#include <QMainWindow> #include <QMainWindow>
#include <QMenu>
#include <QScopedPointer> #include <QScopedPointer>
#include <QSettings>
#include <QString> #include <QString>
#include <QSystemTrayIcon>
#include <QVector> #include <QVector>
#include <QWebEnginePage> #include <QWebEnginePage>
#include <QWebEngineProfile> #include <QWebEngineProfile>
@ -14,17 +17,33 @@ class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
public: public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
static MainWindow *instance(); static MainWindow *instance();
QSettings *settings() const;
private: private:
void setupWebView(); void setupWebView();
void setupTrayIcon();
void cleanTrayIcon();
void setupSettings();
QWebEngineView *m_webView; QWebEngineView *m_webView;
QWebEngineProfile *prepareProfile(); QWebEngineProfile *prepareProfile();
DiscordPage *m_discordPage; DiscordPage *m_discordPage;
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
QSystemTrayIcon *m_trayIcon = nullptr;
QMenu *m_trayIconMenu;
QSettings *m_settings;
bool m_wasMaximized; bool m_wasMaximized;
static MainWindow *m_instance; static MainWindow *m_instance;
bool m_useNotifySend;
#ifdef KNOTIFICATIONS
bool m_useKF5Notifications = true;
#else
bool m_useKF5Notifications = false;
#endif
public Q_SLOTS:
void setTrayIcon(bool enabled);
private Q_SLOTS: private Q_SLOTS:
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);

View File

@ -9,73 +9,95 @@
#include <QSizePolicy> #include <QSizePolicy>
#include <QVBoxLayout> #include <QVBoxLayout>
StreamDialog::StreamDialog() : QWidget() { StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) {
setAttribute(Qt::WA_QuitOnClose, false); setAttribute(Qt::WA_QuitOnClose, false);
{
auto layout = new QVBoxLayout(this); auto layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize); layout->setSizeConstraint(QLayout::SetFixedSize);
auto targetLabel = new QLabel(this); m_videoGroupBox = new QGroupBox(this);
targetLabel->setText("Which app do you want to stream sound from?"); m_videoGroupBox->setTitle("Video");
layout->addWidget(targetLabel); m_videoGroupBox->setCheckable(true);
layout->addWidget(m_videoGroupBox);
auto targetHBox = new QHBoxLayout(this); {
layout->addLayout(targetHBox); auto videoLayout = new QVBoxLayout(m_videoGroupBox);
auto resolutionLabel = new QLabel(this);
resolutionLabel->setText("Resolution");
videoLayout->addWidget(resolutionLabel);
m_resolutionComboBox = new QComboBox(this);
m_resolutionComboBox->addItem("2160p", "3840x2160");
m_resolutionComboBox->addItem("1440p", "2560x1440");
m_resolutionComboBox->addItem("1080p", "1920x1080");
m_resolutionComboBox->addItem("720p", "1280x720");
m_resolutionComboBox->addItem("480p", "854x480");
m_resolutionComboBox->addItem("360p", "640x360");
m_resolutionComboBox->addItem("240p", "426x240");
m_resolutionComboBox->setCurrentText("720p");
videoLayout->addWidget(m_resolutionComboBox);
auto framerateLabel = new QLabel(this);
framerateLabel->setText("Framerate");
videoLayout->addWidget(framerateLabel);
m_framerateComboBox = new QComboBox(this);
m_framerateComboBox->addItem("144 FPS", 144);
m_framerateComboBox->addItem("60 FPS", 60);
m_framerateComboBox->addItem("30 FPS", 30);
m_framerateComboBox->addItem("15 FPS", 15);
m_framerateComboBox->addItem("5 FPS", 5);
m_framerateComboBox->setCurrentText("30 FPS");
videoLayout->addWidget(m_framerateComboBox);
}
m_audioGroupBox = new QGroupBox(this);
m_audioGroupBox->setCheckable(true);
m_audioGroupBox->setTitle("Audio");
layout->addWidget(m_audioGroupBox);
{
auto audioLayout = new QVBoxLayout(m_audioGroupBox);
auto targetLabel = new QLabel(this);
targetLabel->setText("Audio Source");
audioLayout->addWidget(targetLabel);
{
auto targetLayout = new QHBoxLayout();
audioLayout->addLayout(targetLayout);
m_targetComboBox = new QComboBox(this); m_targetComboBox = new QComboBox(this);
updateTargets(); updateTargets();
targetHBox->addWidget(m_targetComboBox); targetLayout->addWidget(m_targetComboBox);
auto refreshTargetsButton = new QPushButton(this); auto refreshTargetsButton = new QPushButton(this);
refreshTargetsButton->setFixedSize(30, 30); refreshTargetsButton->setFixedSize(30, 30);
refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
connect(refreshTargetsButton, &QPushButton::clicked, this, connect(refreshTargetsButton, &QPushButton::clicked, this,
&StreamDialog::updateTargets); &StreamDialog::updateTargets);
targetHBox->addWidget(refreshTargetsButton); targetLayout->addWidget(refreshTargetsButton);
}
auto qualityLabel = new QLabel(this); }
qualityLabel->setText("Stream Quality");
layout->addWidget(qualityLabel);
auto qualityHBox = new QHBoxLayout(this);
layout->addLayout(qualityHBox);
m_qualityResolutionComboBox = new QComboBox(this);
m_qualityResolutionComboBox->addItem("2160p", "3840x2160");
m_qualityResolutionComboBox->addItem("1440p", "2560x1440");
m_qualityResolutionComboBox->addItem("1080p", "1920x1080");
m_qualityResolutionComboBox->addItem("720p", "1280x720");
m_qualityResolutionComboBox->addItem("480p", "854x480");
m_qualityResolutionComboBox->addItem("360p", "640x360");
m_qualityResolutionComboBox->addItem("240p", "426x240");
m_qualityResolutionComboBox->setCurrentText("720p");
qualityHBox->addWidget(m_qualityResolutionComboBox);
m_qualityFPSComboBox = new QComboBox(this);
m_qualityFPSComboBox->addItem("144 FPS", 144);
m_qualityFPSComboBox->addItem("60 FPS", 60);
m_qualityFPSComboBox->addItem("30 FPS", 30);
m_qualityFPSComboBox->addItem("15 FPS", 15);
m_qualityFPSComboBox->addItem("5 FPS", 5);
m_qualityFPSComboBox->setCurrentText("30 FPS");
qualityHBox->addWidget(m_qualityFPSComboBox);
auto button = new QPushButton(this); auto button = new QPushButton(this);
button->setText("Start Stream"); button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
}
setLayout(layout);
setWindowTitle("discord-screenaudio Stream Dialog"); setWindowTitle("discord-screenaudio Stream Dialog");
} }
void StreamDialog::startStream() { void StreamDialog::startStream() {
auto resolution = auto resolution = m_resolutionComboBox->currentData().toString().split('x');
m_qualityResolutionComboBox->currentData().toString().split('x'); emit requestedStreamStart(m_videoGroupBox->isChecked(),
emit requestedStreamStart(m_targetComboBox->currentText(), m_audioGroupBox->isChecked(), resolution[0].toInt(),
resolution[0].toUInt(), resolution[1].toUInt(), resolution[1].toInt(),
m_qualityFPSComboBox->currentData().toUInt()); m_framerateComboBox->currentData().toInt(),
m_targetComboBox->currentText());
setHidden(true); setHidden(true);
} }
@ -83,7 +105,6 @@ void StreamDialog::updateTargets() {
auto lastTarget = m_targetComboBox->currentText(); auto lastTarget = m_targetComboBox->currentText();
m_targetComboBox->clear(); m_targetComboBox->clear();
m_targetComboBox->addItem("[None]");
m_targetComboBox->addItem("[All Desktop Audio]"); m_targetComboBox->addItem("[All Desktop Audio]");
for (auto target : Virtmic::getTargets()) { for (auto target : Virtmic::getTargets()) {
m_targetComboBox->addItem(target); m_targetComboBox->addItem(target);

View File

@ -2,22 +2,25 @@
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QGroupBox>
#include <QWidget> #include <QWidget>
class StreamDialog : public QWidget { class StreamDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit StreamDialog(); explicit StreamDialog(QWidget *parent = nullptr);
private: private:
QComboBox *m_targetComboBox; QComboBox *m_targetComboBox;
QComboBox *m_qualityResolutionComboBox; QComboBox *m_resolutionComboBox;
QComboBox *m_qualityFPSComboBox; QComboBox *m_framerateComboBox;
QGroupBox *m_videoGroupBox;
QGroupBox *m_audioGroupBox;
Q_SIGNALS: Q_SIGNALS:
void requestedStreamStart(QString target, uint width, uint height, void requestedStreamStart(bool video, bool audio, int width, int height,
uint frameRate); int frameRate, QString target);
public Q_SLOTS: public Q_SLOTS:
void updateTargets(); void updateTargets();

234
src/userscript.cpp Normal file
View File

@ -0,0 +1,234 @@
#include "userscript.h"
#include "log.h"
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QTimer>
#ifdef KXMLGUI
#include <KActionCollection>
#endif
UserScript::UserScript() : QObject() {
setupHelpMenu();
setupShortcutsDialog();
setupStreamDialog();
setupVirtmic();
}
void UserScript::setupHelpMenu() {
#ifdef KXMLGUI
m_kxmlgui = true;
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
m_helpMenu = new KHelpMenu(MainWindow::instance(), aboutData);
#endif
}
void UserScript::setupShortcutsDialog() {
#ifdef KXMLGUI
#ifdef KGLOBALACCEL
m_kglobalaccel = true;
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&UserScript::muteToggled);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&UserScript::deafenToggled);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
}
void UserScript::setupStreamDialog() {
m_streamDialog = new StreamDialog(MainWindow::instance());
connect(m_streamDialog, &StreamDialog::requestedStreamStart, this,
&UserScript::startStream);
}
void UserScript::setupVirtmic() {
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
}
bool UserScript::isVirtmicRunning() {
return m_virtmicProcess.state() != QProcess::NotRunning;
}
QString UserScript::version() { return QApplication::applicationVersion(); }
QVariant UserScript::getPref(QString name, QVariant fallback) {
return MainWindow::instance()->settings()->value(name, fallback);
}
bool UserScript::getBoolPref(QString name, bool fallback) {
return getPref(name, fallback).toBool();
}
void UserScript::setPref(QString name, QVariant value) {
return MainWindow::instance()->settings()->setValue(name, value);
}
void UserScript::setTrayIcon(bool value) {
setPref("trayIcon", value);
MainWindow::instance()->setTrayIcon(value);
}
void UserScript::log(QString message) {
qDebug(userscriptLog) << message.toUtf8().constData();
}
void UserScript::showShortcutsDialog() {
#ifdef KXMLGUI
#ifdef KGLOBALACCEL
m_shortcutsDialog->show();
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KGlobalAccel is not available).",
QMessageBox::Ok);
#endif
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KXmlGui and KGlobalAccel are not available).",
QMessageBox::Ok);
#endif
}
void UserScript::showHelpMenu() {
#ifdef KXMLGUI
m_helpMenu->aboutApplication();
#endif
}
void UserScript::stopVirtmic() {
if (m_virtmicProcess.state() == QProcess::Running) {
qDebug(virtmicLog) << "Stopping Virtmic";
m_virtmicProcess.kill();
m_virtmicProcess.waitForFinished();
}
}
void UserScript::startVirtmic(QString target) {
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
void UserScript::startStream(bool video, bool audio, int width, int height,
int frameRate, QString target) {
stopVirtmic();
startVirtmic(audio ? target : "[None]");
// Wait a bit for the virtmic to start
QTimer::singleShot(
200, [=]() { emit streamStarted(video, width, height, frameRate); });
}
void UserScript::showStreamDialog() {
if (m_streamDialog->isHidden())
m_streamDialog->setHidden(false);
else
m_streamDialog->activateWindow();
m_streamDialog->updateTargets();
}
QVariant UserScript::vencordSend(QString event, QVariantList args) {
QString configFolder =
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
"/vencord";
QString quickCssFile = configFolder + "/quickCss.css";
QString settingsFile = configFolder + "/settings.json";
if (!QDir().exists(configFolder))
QDir().mkpath(configFolder);
if (event == "VencordGetRepo") {
return true;
}
if (event == "VencordGetSettingsDir") {
return configFolder;
}
if (event == "VencordGetQuickCss") {
if (QFile::exists(quickCssFile)) {
QFile file(quickCssFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
quickCssFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "";
}
if (event == "VencordGetSettings") {
if (QFile::exists(settingsFile)) {
QFile file(settingsFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "{}";
}
if (event == "VencordSetSettings") {
QFile file(settingsFile);
if (!file.open(QIODevice::WriteOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
file.write(args[0].toString().toUtf8());
file.close();
return true;
}
if (event == "VencordGetUpdates") {
return QVariantMap{{"ok", true}, {"value", QVariantList()}};
}
if (event == "VencordOpenExternal") {
QDesktopServices::openUrl(QUrl(args[0].toString()));
return true;
}
if (event == "VencordOpenQuickCss") {
return true;
}
assert(false);
}

71
src/userscript.h Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include "streamdialog.h"
#include <QObject>
#include <QProcess>
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
class UserScript : public QObject {
Q_OBJECT
public:
UserScript();
bool isVirtmicRunning();
Q_PROPERTY(QString version READ version CONSTANT);
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT);
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT);
private:
QProcess m_virtmicProcess;
StreamDialog *m_streamDialog;
bool m_kxmlgui = false;
bool m_kglobalaccel = false;
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
QString m_vencordSettings;
void setupHelpMenu();
void setupShortcutsDialog();
void setupStreamDialog();
void setupVirtmic();
Q_SIGNALS:
void muteToggled();
void deafenToggled();
void streamStarted(bool video, int width, int height, int frameRate);
public Q_SLOTS:
void log(QString message);
QString version();
QVariant getPref(QString name, QVariant fallback);
bool getBoolPref(QString name, bool fallback);
void setPref(QString name, QVariant value);
void setTrayIcon(bool value);
void showShortcutsDialog();
void showHelpMenu();
void showStreamDialog();
void stopVirtmic();
void startVirtmic(QString target);
QVariant vencordSend(QString event, QVariantList args);
private Q_SLOTS:
void startStream(bool video, bool audio, int width, int height, int frameRate,
QString target);
};

View File

@ -22,7 +22,13 @@ QVector<QString> getTargets() {
if (global.type == pipewire::node::type) { if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id); auto node = reg.bind<pipewire::node>(global.id);
auto info = node.info(); auto info = node.info();
auto name = QString::fromStdString(info.props["application.name"]); QString name;
if (info.props.count("application.name") &&
info.props["application.name"] != "")
name = QString::fromStdString(info.props["application.name"]);
else
name = QString::fromStdString(
info.props["application.process.binary"]);
if (name != "" && !EXCLUDE_TARGETS.contains(name) && if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
!targets.contains(name)) { !targets.contains(name)) {
@ -67,7 +73,12 @@ void start(QString _target) {
continue; continue;
auto &parent = nodes.at(parent_id); auto &parent = nodes.at(parent_id);
auto name = parent.props["application.name"]; std::string name;
if (parent.props.count("application.name") &&
parent.props["application.name"] != "")
name = parent.props["application.name"];
else
name = parent.props["application.process.binary"];
if (name == target || if (name == target ||
(target == "[All Desktop Audio]" && (target == "[All Desktop Audio]" &&
@ -78,8 +89,7 @@ void start(QString _target) {
core.create<pipewire::link_factory>( core.create<pipewire::link_factory>(
{fl ? virt_fl->info().id : virt_fr->info().id, port_id})); {fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
.arg(QString::fromStdString( .arg(QString::fromStdString(name))
parent.props["application.name"]))
.arg(port_id) .arg(port_id)
.arg(fl ? virt_fl->info().id .arg(fl ? virt_fl->info().id
: virt_fr->info().id) : virt_fr->info().id)
@ -112,11 +122,17 @@ void start(QString _target) {
[&](const pipewire::global &global) { [&](const pipewire::global &global) {
if (global.type == pipewire::node::type) { if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id); auto node = reg.bind<pipewire::node>(global.id);
if (!node.info().props.count("application.name")) auto info = node.info();
std::string name;
if (info.props.count("application.name") &&
info.props["application.name"] != "")
name = info.props["application.name"];
else if (info.props.count("application.process.binary")) {
name = info.props["application.process.binary"];
} else
return; return;
qDebug(virtmicLog) << QString("Added: %1") qDebug(virtmicLog) << QString("Added: %1")
.arg(QString::fromStdString( .arg(QString::fromStdString(name))
node.info().props["application.name"]))
.toUtf8() .toUtf8()
.data(); .data();
@ -152,9 +168,14 @@ void start(QString _target) {
[&](const std::uint32_t id) { [&](const std::uint32_t id) {
if (nodes.count(id)) { if (nodes.count(id)) {
auto info = nodes.at(id); auto info = nodes.at(id);
std::string name;
if (info.props.count("application.name") &&
info.props["application.name"] != "")
name = info.props["application.name"];
else
name = info.props["application.process.binary"];
qDebug(virtmicLog) << QString("Removed: %1") qDebug(virtmicLog) << QString("Removed: %1")
.arg(QString::fromStdString( .arg(QString::fromStdString(name))
info.props["application.name"].data()))
.toUtf8() .toUtf8()
.data(); .data();
nodes.erase(id); nodes.erase(id);

View File

@ -1,73 +0,0 @@
#include "webclass.h"
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QUrl>
QVariant WebClass::vencordSend(QString event, QVariantList args) {
QString configFolder =
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
"/vencord";
QString quickCssFile = configFolder + "/quickCss.css";
QString settingsFile = configFolder + "/settings.json";
if (!QDir().exists(configFolder))
QDir().mkpath(configFolder);
if (event == "VencordGetRepo") {
return true;
}
if (event == "VencordGetSettingsDir") {
return configFolder;
}
if (event == "VencordGetQuickCss") {
if (QFile::exists(quickCssFile)) {
QFile file(quickCssFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
quickCssFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "";
}
if (event == "VencordGetSettings") {
if (QFile::exists(settingsFile)) {
QFile file(settingsFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "{}";
}
if (event == "VencordSetSettings") {
QFile file(settingsFile);
if (!file.open(QIODevice::WriteOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
file.write(args[0].toString().toUtf8());
file.close();
return true;
}
if (event == "VencordGetUpdates") {
return QVariantMap{{"ok", true}, {"value", QVariantList()}};
}
if (event == "VencordOpenExternal") {
QDesktopServices::openUrl(QUrl(args[0].toString()));
return true;
}
if (event == "VencordOpenQuickCss") {
return true;
}
assert(false);
}

View File

@ -1,13 +0,0 @@
#pragma once
#include <QObject>
#include <QVariant>
class WebClass : public QObject {
Q_OBJECT
public slots:
QVariant vencordSend(QString event, QVariantList args);
private:
QString m_vencordSettings;
};