diff --git a/CMakeLists.txt b/CMakeLists.txt index b664337..9feccc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") string(TIMESTAMP TIMESTAMP %s) # set(CMAKE_AUTOUIC ON) -find_package(Qt5 CONFIG REQUIRED COMPONENTS - Widgets - WebEngineWidgets -) +find_package(Qt5 COMPONENTS Widgets) +if (Qt5_FOUND) + find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets) -find_package(KF5Notifications) -if(KF5Notifications_FOUND) - add_definitions( -DKNOTIFICATIONS ) -endif() + find_package(KF5Notifications) + if(KF5Notifications_FOUND) + add_definitions( -DKNOTIFICATIONS ) + endif() -find_package(KF5XmlGui) -if(KF5XmlGui_FOUND) - add_definitions( -DKXMLGUI ) -endif() + find_package(KF5XmlGui) + if(KF5XmlGui_FOUND) + add_definitions( -DKXMLGUI ) + endif() -find_package(KF5GlobalAccel) -if(KF5GlobalAccel_FOUND) - add_definitions( -DKGLOBALACCEL ) + find_package(KF5GlobalAccel) + if(KF5GlobalAccel_FOUND) + 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() set(discord-screenaudio_SRC @@ -40,7 +43,7 @@ set(discord-screenaudio_SRC src/discordpage.cpp src/streamdialog.cpp src/log.cpp - src/webclass.cpp + src/userscript.cpp resources.qrc ) @@ -67,7 +70,7 @@ add_subdirectory(submodules/rohrkabel) 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) target_link_libraries(discord-screenaudio KF5::Notifications) diff --git a/README.md b/README.md index ee83eaa..65dbe21 100644 --- a/README.md +++ b/README.md @@ -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 ([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 Discord client. Rather, it should be used in addition to the original client in @@ -50,6 +50,8 @@ You have multiple options: ### Requirements - Basic building tools +- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu + 20/21) - CMake - Qt5 and QtWebEngine - **PipeWire** (it currently doesn't work with PulseAudio) @@ -57,7 +59,7 @@ You have multiple options: - _Kf5Notifications (optional, for better notifications)_ - _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` ### Building @@ -82,7 +84,9 @@ And then to optionally install it, run: sudo cmake --install build ``` -## How it works +## FAQ + +### How does this work? This whole project is based on [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 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 Copyright (C) 2022 Malte Jürgens diff --git a/assets/de.shorsh.discord-screenaudio.metainfo.xml.in b/assets/de.shorsh.discord-screenaudio.metainfo.xml.in index bd81780..7d459dc 100644 --- a/assets/de.shorsh.discord-screenaudio.metainfo.xml.in +++ b/assets/de.shorsh.discord-screenaudio.metainfo.xml.in @@ -4,6 +4,7 @@ CC0-1.0 GPL-3.0+ discord-screenaudio + Malte Jürgens diff --git a/assets/userscript.js b/assets/userscript.js index cf00169..b8605a6 100644 --- a/assets/userscript.js +++ b/assets/userscript.js @@ -1,5 +1,3 @@ -// From v0.4 - navigator.mediaDevices.chromiumGetDisplayMedia = navigator.mediaDevices.getDisplayMedia; @@ -16,16 +14,16 @@ const getAudioDevice = async (nameOfAudioDevice) => { let devices = await navigator.mediaDevices.enumerateDevices(); audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); if (!audioDevice) - console.log( - `dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms` + userscript.log( + `Did not find '${nameOfAudioDevice}', trying again in 100ms` ); await sleep(100); } - console.log(`dsa: Found '${nameOfAudioDevice}'`); + userscript.log(`Found '${nameOfAudioDevice}'`); return audioDevice; }; -function setGetDisplayMedia(overrideArgs = undefined) { +function setGetDisplayMedia(video = true, overrideArgs = undefined) { const getDisplayMedia = async (...args) => { var id; try { @@ -63,6 +61,7 @@ function setGetDisplayMedia(overrideArgs = undefined) { : args || [{ video: true, audio: true }]) ); gdm.addTrack(track); + if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false; return gdm; }; navigator.mediaDevices.getDisplayMedia = getDisplayMedia; @@ -70,137 +69,275 @@ function setGetDisplayMedia(overrideArgs = undefined) { setGetDisplayMedia(); +let userscript; +let muteBtn; +let deafenBtn; +let streamStartBtn; +let streamStartBtnInitialDisplay; +let streamStartBtnClone; +let resolutionString; const clonedElements = []; const hiddenElements = []; let wasStreamActive = false; -setInterval(() => { - const streamActive = - document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") - .length > 0; +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; +} - if (!streamActive && wasStreamActive) - console.log("!discord-screenaudio-stream-stopped"); - wasStreamActive = streamActive; +function createSwitch(text, enabled, onClick) { + const container = document.createElement("div"); + container.style.marginBottom = "20px"; + container.className = "labelRow-NnoUIp"; - if (streamActive) { - clonedElements.forEach((el) => { - el.remove(); - }); - clonedElements.length = 0; + const label = document.createElement("label"); + label.innerText = text; + label.className = "title-2yADjX"; + container.appendChild(label); - hiddenElements.forEach((el) => { - el.style.display = "block"; - }); - hiddenElements.length = 0; - } else { - for (const el of [ - document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1], - document.querySelector( - ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" - ), - ]) { - if (!el) continue; - if (el.classList.contains("discord-screenaudio-cloned")) continue; - el.classList.add("discord-screenaudio-cloned"); - elClone = el.cloneNode(true); - elClone.title = "Share Your Screen with Audio"; - elClone.addEventListener("click", () => { - console.log("!discord-screenaudio-start-stream"); - }); + const svg = document.createElement("div"); + container.appendChild(svg); - const initialDisplay = el.style.display; - - window.discordScreenaudioStartStream = (width, height, frameRate) => { - window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`; - setGetDisplayMedia({ - audio: true, - video: { width, height, frameRate }, - }); - el.click(); - el.style.display = initialDisplay; - elClone.remove(); - }; - - el.style.display = "none"; - el.parentNode.insertBefore(elClone, el); - - clonedElements.push(elClone); - hiddenElements.push(el); - } + function setSvgDisabled() { + svg.innerHTML = `
`; } - // Add about text in settings - if ( - document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0 - ) { - for (const el of document.getElementsByClassName("info-3pQQBb")) { - let aboutEl; - if (window.discordScreenaudioKXMLGUI) { - aboutEl = document.createElement("a"); - aboutEl.addEventListener("click", () => { - console.log("!discord-screenaudio-about"); - }); - } else { - aboutEl = document.createElement("div"); - } - aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`; - aboutEl.style.fontSize = "12px"; - aboutEl.style.color = "var(--text-muted)"; - aboutEl.style.textTransform = "none"; - aboutEl.classList.add("dirscordScreenaudioAboutText"); - aboutEl.style.cursor = "pointer"; - el.appendChild(aboutEl); - } + function setSvgEnabled() { + svg.innerHTML = `
`; } - // Remove stream settings if stream is active - document.getElementById("manage-streams-change-windows")?.remove(); - document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); - - // Add event listener for keybind tab - if ( - document - .getElementById("keybinds-tab") - ?.getElementsByClassName( - "container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o" - ).length - ) { - const el = document - .getElementById("keybinds-tab") - .getElementsByClassName("children-1xdcWE")[0]; - const div = document.createElement("div"); - div.style.marginBottom = "50px"; - const button = document.createElement("button"); - button.classList = - "button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F"; - button.innerText = "Edit Global Keybinds"; - button.addEventListener("click", () => { - console.log("!discord-screenaudio-keybinds"); - }); - div.appendChild(button); - el.innerHTML = ""; - el.appendChild(div); + function updateSvg() { + if (enabled) setSvgEnabled(); + else setSvgDisabled(); } - const muteBtn = document.getElementsByClassName( - "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" - )[0]; - window.discordScreenaudioToggleMute = () => muteBtn.click(); - const deafenBtn = document.getElementsByClassName( - "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" - )[1]; - window.discordScreenaudioToggleDeafen = () => deafenBtn.click(); + container.addEventListener("click", () => { + enabled = !enabled; + updateSvg(); + onClick(enabled); + }); + updateSvg(); - if (window.discordScreenaudioResolutionString) { - for (const el of document.getElementsByClassName( - "qualityIndicator-39wQDy" - )) { - el.innerHTML = window.discordScreenaudioResolutionString; - } - } -}, 500); + 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 = + document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") + .length > 0; + + if (!streamActive && wasStreamActive) userscript.stopVirtmic(); + wasStreamActive = streamActive; + + if (streamActive) { + clonedElements.forEach((el) => { + el.remove(); + }); + clonedElements.length = 0; + + hiddenElements.forEach((el) => { + el.style.display = "block"; + }); + hiddenElements.length = 0; + } else { + for (const el of [ + document.getElementsByClassName("actionButtons-2vEOUh")?.[0] + ?.children[1], + document.querySelector( + ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" + ), + ]) { + if (!el) continue; + if (el.classList.contains("discord-screenaudio-cloned")) continue; + streamStartBtn = el; + streamStartBtn.classList.add("discord-screenaudio-cloned"); + + streamStartBtnClone = streamStartBtn.cloneNode(true); + streamStartBtnClone.title = "Share Your Screen with Audio"; + streamStartBtnClone.addEventListener("click", () => { + userscript.showStreamDialog(); + }); + + streamStartBtnInitialDisplay = streamStartBtn.style.display; + + streamStartBtn.style.display = "none"; + streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el); + + clonedElements.push(streamStartBtnClone); + hiddenElements.push(streamStartBtn); + } + } + + // Add about text in settings + if ( + document.getElementsByClassName("dirscordScreenaudioAboutText").length == + 0 + ) { + for (const el of document.getElementsByClassName("info-3pQQBb")) { + let aboutEl; + if (userscript.kxmlgui) { + aboutEl = document.createElement("a"); + aboutEl.addEventListener("click", () => { + userscript.showHelpMenu(); + }); + } else { + aboutEl = document.createElement("div"); + } + aboutEl.innerText = `discord-screenaudio ${userscript.version}`; + aboutEl.style.fontSize = "12px"; + aboutEl.style.color = "var(--text-muted)"; + aboutEl.style.textTransform = "none"; + aboutEl.style.display = "inline-block"; + aboutEl.style.width = "100%"; + aboutEl.classList.add("dirscordScreenaudioAboutText"); + aboutEl.style.cursor = "pointer"; + el.appendChild(aboutEl); + } + } + + // Remove stream settings if stream is active + document.getElementById("manage-streams-change-windows")?.remove(); + document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); + + // Add event listener for keybind tab + if ( + document + .getElementById("keybinds-tab") + ?.getElementsByClassName( + "container-3jbRo5 info-1hMolH browserNotice-1u-Y5o" + ).length + ) { + const el = document + .getElementById("keybinds-tab") + .getElementsByClassName("children-1xdcWE")[0]; + const div = document.createElement("div"); + div.style.marginBottom = "50px"; + div.appendChild( + createButton("Edit Global Keybinds", () => { + userscript.showShortcutsDialog(); + }) + ); + el.innerHTML = ""; + el.appendChild(div); + } + + const buttonContainer = + document.getElementsByClassName("container-YkUktl")[0]; + if (!buttonContainer) { + userscript.log( + "Cannot locate Mute/Deafen/Settings button container, please report this on GitHub" + ); + } + + 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( + "qualityIndicator-39wQDy" + )) { + el.innerHTML = resolutionString; + } + } + + const accountTab = document.getElementById("my-account-tab"); + if (accountTab) { + 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); +} diff --git a/src/discordpage.cpp b/src/discordpage.cpp index 7a0712a..b5005f9 100644 --- a/src/discordpage.cpp +++ b/src/discordpage.cpp @@ -3,22 +3,10 @@ #include "mainwindow.h" #include "virtmic.h" -#ifdef KXMLGUI -#include -#include -#include -#include -#include - -#ifdef KGLOBALACCEL -#include -#endif - -#endif - #include #include #include +#include #include #include #include @@ -29,16 +17,40 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { setBackgroundColor(QColor("#202225")); - m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels); connect(this, &QWebEnginePage::featurePermissionRequested, this, &DiscordPage::featurePermissionRequested); - connect(this, &QWebEnginePage::loadStarted, [=]() { - runJavaScript(QString("window.discordScreenaudioVersion = '%1';") - .arg(QApplication::applicationVersion())); - }); + setupPermissions(); + 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::JavascriptCanOpenWindows, true); settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, @@ -50,86 +62,19 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { false); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); 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{}); - m_actionCollection->addAction("toggleDeafen", toggleDeafenAction); - KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList{}); - - 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, QWebEngineScript::InjectionPoint injectionPoint) { qDebug(mainLog) << "Injecting " << name; @@ -145,16 +90,31 @@ void DiscordPage::injectScriptText( scripts().insert(script); } -void DiscordPage::injectScriptFile( - QString name, QString source, - QWebEngineScript::InjectionPoint injectionPoint) { +void DiscordPage::injectScript(QString name, QString content) { + injectScript(name, content, QWebEngineScript::DocumentCreation); +} + +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); if (!file.open(QIODevice::ReadOnly)) { qFatal("Failed to load %s with error: %s", source.toLatin1().constData(), file.errorString().toLatin1().constData()); } else { - injectScriptText(name, file.readAll(), injectionPoint); + (this->*inject)(name, file.readAll()); } } @@ -165,11 +125,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::PermissionGrantedByUser); 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 " "Discord can find all the audio devices"; - m_virtmicProcess.start(QApplication::arguments()[0], - {"--virtmic", "None"}); + m_userScript.startVirtmic("None"); } } } @@ -196,78 +155,81 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) { return new ExternalPage; } -void DiscordPage::stopVirtmic() { - if (m_virtmicProcess.state() == QProcess::Running) { - qDebug(virtmicLog) << "Stopping Virtmic"; - m_virtmicProcess.kill(); - m_virtmicProcess.waitForFinished(); - } -} - -void DiscordPage::startVirtmic(QString target) { - if (target != "None") { - qDebug(virtmicLog) << "Starting Virtmic with target" << target; - m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target}); - } -} +const QMap cssAnsiColorMap = {{"black", "30"}, + {"red", "31"}, + {"green", "32"}, + {"yellow", "33"}, + {"blue", "34"}, + {"magenta", "35"}, + {"cyan", "36"}, + {"white", "37"}, + {"gray", "90"}, + {"bright-red", "91"}, + {"bright-green", "92"}, + {"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( QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceID) { - if (message == "!discord-screenaudio-start-stream") { - if (m_streamDialog.isHidden()) - m_streamDialog.setHidden(false); - else - m_streamDialog.activateWindow(); - m_streamDialog.updateTargets(); - } else if (message == "!discord-screenaudio-stream-stopped") { - stopVirtmic(); - } else if (message == "!discord-screenaudio-about") { -#ifdef KXMLGUI - m_helpMenu->aboutApplication(); -#endif - } else if (message == "!discord-screenaudio-keybinds") { -#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 - } else if (message.startsWith("dsa: ")) { - qDebug(userscriptLog) << message.mid(5).toUtf8().constData(); - } else { - qDebug(discordLog) << message; + auto colorSegments = message.split("%c"); + for (auto segment : colorSegments.mid(1)) { + auto lines = segment.split("\n"); + QString ansi; + uint endOfStyles = lines.length(); + for (size_t line = 1; line < lines.length(); line++) { + if (!lines[line].endsWith(";")) { + endOfStyles = line; + break; + } + if (lines[line] == "font-weight: bold;") + ansi += "\033[1m"; + else if (lines[line].startsWith("color: ")) { + auto color = lines[line].mid(7).chopped(1); + if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end()) + ansi += "\033[" + cssAnsiColorMap[color] + "m"; + } + } + qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " + + ((lines.length() > endOfStyles) + ? lines[endOfStyles].trimmed() + : "")) + .toUtf8() + .constData(); + for (auto line : lines.mid(endOfStyles + 1)) { + qDebug(discordLog) << line.toUtf8().constData(); + } } } - -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();"); -} diff --git a/src/discordpage.h b/src/discordpage.h index 42f4e12..3c23fb2 100644 --- a/src/discordpage.h +++ b/src/discordpage.h @@ -1,16 +1,9 @@ #pragma once #include "streamdialog.h" +#include "userscript.h" #include "virtmic.h" -#include "webclass.h" -#ifdef KXMLGUI -#include -#include -#include -#endif - -#include #include #include #include @@ -22,16 +15,9 @@ public: explicit DiscordPage(QWidget *parent = nullptr); private: - StreamDialog m_streamDialog; - QProcess m_virtmicProcess; - WebClass m_webClass; -#ifdef KXMLGUI - KHelpMenu *m_helpMenu; -#ifdef KGLOBALACCEL - KActionCollection *m_actionCollection; - KShortcutsDialog *m_shortcutsDialog; -#endif -#endif + UserScript m_userScript; + void setupPermissions(); + void setupUserStyles(); bool acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame) override; @@ -40,21 +26,16 @@ private: javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceID) override; - void injectScriptText(QString name, QString content, - QWebEngineScript::InjectionPoint injectionPoint = - QWebEngineScript::DocumentCreation); - void injectScriptFile(QString name, QString source, - QWebEngineScript::InjectionPoint injectionPoint = - QWebEngineScript::DocumentCreation); - void stopVirtmic(); - void startVirtmic(QString target); - void toggleMute(); - void toggleDeafen(); + void injectScript(QString name, QString content, + QWebEngineScript::InjectionPoint injectionPoint); + void injectScript(QString name, QString content); + void injectStylesheet(QString name, QString content); + void injectFile(void (DiscordPage::*inject)(QString, QString), QString name, + QString source); private Q_SLOTS: void featurePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature); - void startStream(QString target, uint width, uint height, uint frameRate); }; // Will immediately get destroyed again but is needed for navigation to diff --git a/src/main.cpp b/src/main.cpp index e2cd74b..df1fec8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,6 +30,9 @@ int main(int argc, char *argv[]) { QCommandLineOption degubOption("remote-debugging", "Open Chromium Remote Debugging on port 9222"); parser.addOption(degubOption); + QCommandLineOption notifySendOption( + "notify-send", "Use notify-send instead of QT/KF5 notifications"); + parser.addOption(notifySendOption); parser.process(app); @@ -46,7 +49,7 @@ int main(int argc, char *argv[]) { "--remote-debugging-port=9222 " + qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); - MainWindow w; + MainWindow w(parser.isSet(notifySendOption)); w.show(); return app.exec(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bcf8f57..092ec18 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,21 @@ MainWindow *MainWindow::m_instance = nullptr; -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { +MainWindow::MainWindow(bool useNotifySend, QWidget *parent) + : QMainWindow(parent) { assert(MainWindow::m_instance == nullptr); MainWindow::m_instance = this; + m_useNotifySend = useNotifySend; + setupSettings(); setupWebView(); + setupTrayIcon(); resize(1000, 700); showMaximized(); + if (m_settings->value("trayIcon", false).toBool() && + m_settings->value("startHidden", false).toBool()) { + hide(); + QTimer::singleShot(0, [=]() { hide(); }); + } } void MainWindow::setupWebView() { @@ -40,22 +50,37 @@ void MainWindow::setupWebView() { m_webView = new QWebEngineView(this); m_webView->setPage(page); + if (m_useKF5Notifications || m_useNotifySend) + QWebEngineProfile::defaultProfile()->setNotificationPresenter( + [&](std::unique_ptr 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 - QWebEngineProfile::defaultProfile()->setNotificationPresenter( - [&](std::unique_ptr notificationInfo) { - 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(); - activateWindow(); - }); - notification->sendEvent(); - }); + 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); } @@ -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; } diff --git a/src/mainwindow.h b/src/mainwindow.h index 39721c8..0a50d62 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -3,8 +3,11 @@ #include "discordpage.h" #include +#include #include +#include #include +#include #include #include #include @@ -14,17 +17,33 @@ class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(QWidget *parent = nullptr); + explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr); static MainWindow *instance(); + QSettings *settings() const; private: void setupWebView(); + void setupTrayIcon(); + void cleanTrayIcon(); + void setupSettings(); QWebEngineView *m_webView; QWebEngineProfile *prepareProfile(); DiscordPage *m_discordPage; void closeEvent(QCloseEvent *event) override; + QSystemTrayIcon *m_trayIcon = nullptr; + QMenu *m_trayIconMenu; + QSettings *m_settings; bool m_wasMaximized; 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: void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); diff --git a/src/streamdialog.cpp b/src/streamdialog.cpp index 5a151fb..e383dea 100644 --- a/src/streamdialog.cpp +++ b/src/streamdialog.cpp @@ -9,73 +9,95 @@ #include #include -StreamDialog::StreamDialog() : QWidget() { +StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) { setAttribute(Qt::WA_QuitOnClose, false); - auto layout = new QVBoxLayout(this); - layout->setSizeConstraint(QLayout::SetFixedSize); + { + auto layout = new QVBoxLayout(this); + layout->setSizeConstraint(QLayout::SetFixedSize); - auto targetLabel = new QLabel(this); - targetLabel->setText("Which app do you want to stream sound from?"); - layout->addWidget(targetLabel); + m_videoGroupBox = new QGroupBox(this); + m_videoGroupBox->setTitle("Video"); + m_videoGroupBox->setCheckable(true); + layout->addWidget(m_videoGroupBox); - auto targetHBox = new QHBoxLayout(this); - layout->addLayout(targetHBox); + { + auto videoLayout = new QVBoxLayout(m_videoGroupBox); - m_targetComboBox = new QComboBox(this); - updateTargets(); - targetHBox->addWidget(m_targetComboBox); + auto resolutionLabel = new QLabel(this); + resolutionLabel->setText("Resolution"); + videoLayout->addWidget(resolutionLabel); - auto refreshTargetsButton = new QPushButton(this); - refreshTargetsButton->setFixedSize(30, 30); - refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); - connect(refreshTargetsButton, &QPushButton::clicked, this, - &StreamDialog::updateTargets); - targetHBox->addWidget(refreshTargetsButton); + 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 qualityLabel = new QLabel(this); - qualityLabel->setText("Stream Quality"); - layout->addWidget(qualityLabel); + auto framerateLabel = new QLabel(this); + framerateLabel->setText("Framerate"); + videoLayout->addWidget(framerateLabel); - auto qualityHBox = new QHBoxLayout(this); - layout->addLayout(qualityHBox); + 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_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_audioGroupBox = new QGroupBox(this); + m_audioGroupBox->setCheckable(true); + m_audioGroupBox->setTitle("Audio"); + layout->addWidget(m_audioGroupBox); - 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 audioLayout = new QVBoxLayout(m_audioGroupBox); - auto button = new QPushButton(this); - button->setText("Start Stream"); - connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); - layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); + auto targetLabel = new QLabel(this); + targetLabel->setText("Audio Source"); + audioLayout->addWidget(targetLabel); - setLayout(layout); + { + auto targetLayout = new QHBoxLayout(); + audioLayout->addLayout(targetLayout); + + m_targetComboBox = new QComboBox(this); + updateTargets(); + targetLayout->addWidget(m_targetComboBox); + + auto refreshTargetsButton = new QPushButton(this); + refreshTargetsButton->setFixedSize(30, 30); + refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); + connect(refreshTargetsButton, &QPushButton::clicked, this, + &StreamDialog::updateTargets); + targetLayout->addWidget(refreshTargetsButton); + } + } + + auto button = new QPushButton(this); + button->setText("Start Stream"); + connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); + layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); + } setWindowTitle("discord-screenaudio Stream Dialog"); } void StreamDialog::startStream() { - auto resolution = - m_qualityResolutionComboBox->currentData().toString().split('x'); - emit requestedStreamStart(m_targetComboBox->currentText(), - resolution[0].toUInt(), resolution[1].toUInt(), - m_qualityFPSComboBox->currentData().toUInt()); + auto resolution = m_resolutionComboBox->currentData().toString().split('x'); + emit requestedStreamStart(m_videoGroupBox->isChecked(), + m_audioGroupBox->isChecked(), resolution[0].toInt(), + resolution[1].toInt(), + m_framerateComboBox->currentData().toInt(), + m_targetComboBox->currentText()); setHidden(true); } @@ -83,7 +105,6 @@ void StreamDialog::updateTargets() { auto lastTarget = m_targetComboBox->currentText(); m_targetComboBox->clear(); - m_targetComboBox->addItem("[None]"); m_targetComboBox->addItem("[All Desktop Audio]"); for (auto target : Virtmic::getTargets()) { m_targetComboBox->addItem(target); diff --git a/src/streamdialog.h b/src/streamdialog.h index c2f833c..4631ea3 100644 --- a/src/streamdialog.h +++ b/src/streamdialog.h @@ -2,22 +2,25 @@ #include #include +#include #include -class StreamDialog : public QWidget { +class StreamDialog : public QDialog { Q_OBJECT public: - explicit StreamDialog(); + explicit StreamDialog(QWidget *parent = nullptr); private: QComboBox *m_targetComboBox; - QComboBox *m_qualityResolutionComboBox; - QComboBox *m_qualityFPSComboBox; + QComboBox *m_resolutionComboBox; + QComboBox *m_framerateComboBox; + QGroupBox *m_videoGroupBox; + QGroupBox *m_audioGroupBox; Q_SIGNALS: - void requestedStreamStart(QString target, uint width, uint height, - uint frameRate); + void requestedStreamStart(bool video, bool audio, int width, int height, + int frameRate, QString target); public Q_SLOTS: void updateTargets(); diff --git a/src/userscript.cpp b/src/userscript.cpp new file mode 100644 index 0000000..c5021b0 --- /dev/null +++ b/src/userscript.cpp @@ -0,0 +1,234 @@ +#include "userscript.h" +#include "log.h" +#include "mainwindow.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef KXMLGUI +#include +#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{}); + m_actionCollection->addAction("toggleDeafen", toggleDeafenAction); + KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList{}); + + 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); +} diff --git a/src/userscript.h b/src/userscript.h new file mode 100644 index 0000000..730dc6f --- /dev/null +++ b/src/userscript.h @@ -0,0 +1,71 @@ +#pragma once + +#include "streamdialog.h" + +#include +#include + +#ifdef KXMLGUI +#include +#include +#include +#include +#include + +#ifdef KGLOBALACCEL +#include +#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); +}; diff --git a/src/virtmic.cpp b/src/virtmic.cpp index ba6d1fc..2c0ba4f 100644 --- a/src/virtmic.cpp +++ b/src/virtmic.cpp @@ -22,7 +22,13 @@ QVector getTargets() { if (global.type == pipewire::node::type) { auto node = reg.bind(global.id); 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) && !targets.contains(name)) { @@ -67,7 +73,12 @@ void start(QString _target) { continue; 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 || (target == "[All Desktop Audio]" && @@ -78,8 +89,7 @@ void start(QString _target) { core.create( {fl ? virt_fl->info().id : virt_fr->info().id, port_id})); qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") - .arg(QString::fromStdString( - parent.props["application.name"])) + .arg(QString::fromStdString(name)) .arg(port_id) .arg(fl ? virt_fl->info().id : virt_fr->info().id) @@ -112,11 +122,17 @@ void start(QString _target) { [&](const pipewire::global &global) { if (global.type == pipewire::node::type) { auto node = reg.bind(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; qDebug(virtmicLog) << QString("Added: %1") - .arg(QString::fromStdString( - node.info().props["application.name"])) + .arg(QString::fromStdString(name)) .toUtf8() .data(); @@ -152,9 +168,14 @@ void start(QString _target) { [&](const std::uint32_t id) { if (nodes.count(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") - .arg(QString::fromStdString( - info.props["application.name"].data())) + .arg(QString::fromStdString(name)) .toUtf8() .data(); nodes.erase(id); diff --git a/src/webclass.cpp b/src/webclass.cpp deleted file mode 100644 index 65ad2a5..0000000 --- a/src/webclass.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "webclass.h" - -#include -#include -#include -#include -#include -#include - -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); -} diff --git a/src/webclass.h b/src/webclass.h deleted file mode 100644 index 8dbc560..0000000 --- a/src/webclass.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -class WebClass : public QObject { - Q_OBJECT -public slots: - QVariant vencordSend(QString event, QVariantList args); - -private: - QString m_vencordSettings; -};