├── requirements.txt ├── discord_overlay ├── __init__.py └── discord_overlay.py ├── discord-overlay.png ├── discord-overlay.desktop ├── setup.py ├── README.md └── COPYING /requirements.txt: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /discord_overlay/__init__.py: -------------------------------------------------------------------------------- 1 | from .discord_overlay import * 2 | -------------------------------------------------------------------------------- /discord-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trigg/DiscordOverlayLinux/HEAD/discord-overlay.png -------------------------------------------------------------------------------- /discord-overlay.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Discord Overlay 3 | Comment=Display discord information above desktop and games 4 | Exec=discord-overlay 5 | Icon=discord-overlay 6 | Terminal=false 7 | Type=Application 8 | Catergories=Network; 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | def readme(): 4 | return open('README.md', 'r').read() 5 | 6 | setup( 7 | name = 'discordoverlaylinux', 8 | author = 'trigg', 9 | version = '0.0.2', 10 | description = 'Unofficial Discord Overlay for Linux', 11 | long_description = readme(), 12 | long_description_content_type = 'text/markdown', 13 | url = 'https://github.com/trigg/DiscordOverlayLinux', 14 | packages = find_packages(), 15 | include_package_data = True, 16 | data_files = [ 17 | ('share/applications', ['discord-overlay.desktop']), 18 | ('share/icons', ['discord-overlay.png']), 19 | ], 20 | install_requires = [ 21 | 'PyQt5', 22 | 'PyQt5-sip', 23 | 'PyQtWebEngine', 24 | 'pyxdg', 25 | ], 26 | entry_points = { 27 | 'console_scripts': [ 28 | 'discord-overlay = discord_overlay.discord_overlay:entrypoint', 29 | ] 30 | }, 31 | classifiers = { 32 | 'Development Status :: 4 - Beta', 33 | 'Environment :: X11 Applications :: Qt', 34 | 'Intended Audience :: End Users/Desktop', 35 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 36 | 'Operating System :: POSIX :: Linux', 37 | 'Programming Language :: Python :: 3 :: Only', 38 | 'Topic :: Communications :: Chat', 39 | 'Topic :: Communications :: Conferencing', 40 | }, 41 | keywords = 'discord overlay linux', 42 | license = 'GPLv3+', 43 | ) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord overlay is deprecated 2 | We are now developing [Discover Overlay](https://github.com/trigg/Discover). We strongly advise you try that instead! 3 | 4 | ## Discord Overlay for Linux 5 | 6 | A QT browser window to overlay Discord activity over the screen. 7 | ![Screenshot](https://user-images.githubusercontent.com/42376598/81101265-274ea100-8f0e-11ea-83dc-1a5476bffe3d.png) 8 | ![Witch It](https://user-images.githubusercontent.com/964775/81019917-99b47800-8e5f-11ea-9514-2b3cef24ebbf.png) 9 | ![Configuration](https://user-images.githubusercontent.com/535772/82892575-a2243e00-9f47-11ea-8d42-0ec08be39441.png) 10 | 11 | 12 | ## Dependencies 13 | 14 | Python `pip` will deal with dependencies, but for posterity the script needs 15 | 16 | `qt5-webengine pyqt5 python-pyqtwebengine python-pyxdg` 17 | 18 | ## Installation 19 | 20 | Manual 21 | ``` 22 | git clone https://github.com/trigg/DiscordOverlayLinux.git 23 | cd DiscordOverlayLinux 24 | python -m pip install PyQtWebEngine 25 | python -m pip install . 26 | ``` 27 | 28 | ## Usage 29 | 30 | On first use a window will show with two buttons "Layout" and "Position" 31 | 32 | The first allows you to choose which widget you want to use as your overlay, and make any changes to the style. 33 | 34 | The second shows you which display and where on it the overlay will be placed. 35 | 36 | Extra Overlays can be added at will 37 | 38 | ## Known Issues 39 | - Unexpected Discord crashes will leave the overlay in the state it was last showing. 40 | - As this uses Discords StreamKit under the hood there is no way to interact with the overlay itself. 41 | 42 | ### Tested configurations 43 | 44 | - Wayfire/Wayland - Works 45 | - Openbox/X11 - Works 46 | - Gnome/X11 - Works 47 | - Gnome/Wayland - Works 48 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a co 236 | -------------------------------------------------------------------------------- /discord_overlay/discord_overlay.py: -------------------------------------------------------------------------------- 1 | # This program is free software: you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation, either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | import sys 14 | import os 15 | import logging 16 | import base64 17 | import re 18 | import json 19 | import signal 20 | from configparser import ConfigParser 21 | from PyQt5 import QtWidgets, QtGui, QtCore 22 | from PyQt5.QtCore import pyqtSlot 23 | from PyQt5.QtWebEngineWidgets import QWebEngineView 24 | from PyQt5.QtWebChannel import QWebChannel 25 | from pathlib import Path 26 | from xdg.BaseDirectory import xdg_config_home 27 | 28 | logger = logging.getLogger(__name__) 29 | signal.signal(signal.SIGINT, signal.SIG_DFL) 30 | 31 | 32 | class ResizingImage(QtWidgets.QLabel): 33 | def __init__(self): 34 | super().__init__() 35 | self.image = None 36 | self.w = 0 37 | self.h = 0 38 | 39 | def setImage(self, image): 40 | self.image = image 41 | self.fillImage() 42 | 43 | def resizeEvent(self, e): 44 | self.w = e.size().width() 45 | self.h = e.size().height() 46 | self.fillImage() 47 | 48 | def sizeHint(self): 49 | if self.image: 50 | return QtCore.QSize(self.image.width() // 2, self.image.height() // 2) 51 | return QtCore.QSize(0, 0) 52 | 53 | def fillImage(self): 54 | if self.image and self.w > 0 and self.h > 0: 55 | self.setPixmap(self.image.scaled(int(self.w), int( 56 | self.h), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)) 57 | 58 | 59 | class AspectRatioWidget(QtWidgets.QWidget): 60 | def __init__(self, widget): 61 | super().__init__() 62 | self.aspect_ratio = 1 63 | self.setLayout(QtWidgets.QBoxLayout( 64 | QtWidgets.QBoxLayout.LeftToRight, self)) 65 | # add spacer, then widget, then spacer 66 | self.layout().addItem(QtWidgets.QSpacerItem(0, 0)) 67 | self.layout().addWidget(widget) 68 | self.layout().addItem(QtWidgets.QSpacerItem(0, 0)) 69 | self.w = 0 70 | self.h = 0 71 | 72 | def updateScreen(self, x, y): 73 | # A different screen with a different aspect ratio 74 | self.aspect_ratio = x / y 75 | self.changePadding() 76 | 77 | def resizeEvent(self, e): 78 | self.w = e.size().width() 79 | self.h = e.size().height() 80 | self.changePadding() 81 | 82 | def changePadding(self): 83 | if self.w < 1 or self.h < 1: 84 | return 85 | if self.w / self.h > self.aspect_ratio: # too wide 86 | self.layout().setDirection(QtWidgets.QBoxLayout.LeftToRight) 87 | widget_stretch = int(self.h * self.aspect_ratio) 88 | outer_stretch = int((self.w - widget_stretch) / 2 + 0.5) 89 | else: # too tall 90 | self.layout().setDirection(QtWidgets.QBoxLayout.TopToBottom) 91 | widget_stretch = int(self.w / self.aspect_ratio) 92 | outer_stretch = int((self.h - widget_stretch) / 2 + 0.5) 93 | 94 | self.layout().setStretch(0, outer_stretch) 95 | self.layout().setStretch(1, widget_stretch) 96 | self.layout().setStretch(2, outer_stretch) 97 | 98 | 99 | class App(QtCore.QObject): 100 | 101 | def __init__(self, our_app): 102 | super().__init__() 103 | self.app = our_app 104 | self.overlays = [] 105 | self.configDir = os.path.join(xdg_config_home, "discord-overlay") 106 | self.streamkitUrlFileName = os.path.join(self.configDir, "discordurl") 107 | self.configFileName = os.path.join(self.configDir, "discoverlay.ini") 108 | self.settings = None 109 | self.presets = None 110 | 111 | def main(self): 112 | os.makedirs(self.configDir, exist_ok=True) 113 | config = ConfigParser(interpolation=None) 114 | config.read(self.configFileName) 115 | for section in config.sections(): 116 | if not section == 'core': 117 | url_list = config.get( 118 | section, 'url', fallback=None) 119 | if url_list: 120 | for url in url_list.split(","): 121 | 122 | overlay = Overlay( 123 | self, self.configFileName, section, url) 124 | overlay.load() 125 | self.overlays.append(overlay) 126 | if len(self.overlays) == 0: 127 | overlay = Overlay(self, self.configFileName, 'main', None) 128 | overlay.load() 129 | self.overlays.append(overlay) 130 | self.showPresetWindow() 131 | self.app.setQuitOnLastWindowClosed(False) 132 | self.createSysTrayIcon() 133 | 134 | def reinit(self): 135 | for overlay in self.overlays: 136 | overlay.delete() 137 | self.trayIcon.hide() 138 | self.__init__(self.app) 139 | self.main() 140 | 141 | def createSysTrayIcon(self): 142 | trayImgBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AUEDxsTIFcmagAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAN+SURBVFjDzZcxaCJpGIafmTuyisVgsDCJiyApR3FAokmjsT5juuC0l2BzjVyfwv5Ic43EW0ijWCb2JtMEZxFGximDMKzhLIaYKcK4zeaKM8ux5A5vN8Pmbef/53t4v3/+eT+B5fUGSAKbwAawCgQXzzzgDrgFboAR8HGZlwpLrFkD8sBWo9EQ0+k0sViMcDhMIBAAYD6fM5vNmEwmDIdDqtXqJ+A9oAF/fgvAXqFQKB4fH5PL5ZhOp1iWhWmaGIaBYRgAKIqCoiikUilkWSYajdLv96nX61xdXfWAi/8LsAao7Xb7bblcxrIsWq0WkiSRz+dJJBJEIhGCwb874HkejuMwHo/RNA3XdVFVFVmWOT8/p1KpfABaz7nxHEAC+FnX9VA8HqfZbCJJEqVSiXg8vtRhsW2bbreL67ocHh5i2zbZbPYB+AMY/xfAGvDLaDQKCYLA3t4enU6HTCbD12gwGHBwcMDFxQWPj48kk8kH4Pd/OiF+sUfVdT0kCAK1Wo1er/fVxQEymQy9Xo9arYYgCOi6HgLUf3Ngr91uF3d3d9nZ2aHX6y1t+TItKRaLXF9fc3l5SaVS+XwwnxxYKxQKxXK5TLPZpNPpvFhxgHg8TqfTodlsUi6XKRQKxUW7+WGx5qd3797F7u/vcRyH/f19Xlrr6+uYpsnKygrb29ucnZ39CFji4obbyuVytFotSqUSfqlUKtFqtcjlcgBbwBsRSDYaDXE6nSJJ0ota/1wrJEliOp3SaDREICkCm+l0GsuyyOfz+K18Po9lWaTTaYBNEdiIxWKYpkkikfAdIJFIYJomsVgMYEMEVsPhMIZhEIlEfAeIRCIYhkE4HAZYFYFgIBDAMIzPd7ufCgaDGIbx9CcNinxniYA3n89RFAXP83wv6HkeiqIwn88BPBG4m81mKIqC4zi+AziOg6IozGYzgDsRuJ1MJqRSKcbjse8A4/GYVCrFZDIBuBWBm+FwiCzLaJrmO4CmaciyzHA4BLgRgVG1Wv0UjUZxXRfbtn0rbts2rusSjUafcuNIXKTX9/1+H1VV6Xa7vgF0u11UVaXf77MIrR+fPkOtXq8jyzKu6zIYDF68+GAwwHVdZFmmXq+zSMzfP5B8mQl/1XX9bSgUolarcXp6+s0Qtm1zdHTEyckJDw8PZLPZD8BvryaUvrpY/ioGk1cxmr2a4dT38fwv9cLeiMwLuMsAAAAASUVORK5CYII=" 143 | pm = QtGui.QPixmap() 144 | pm.loadFromData(base64.b64decode(trayImgBase64)) 145 | 146 | self.trayIcon = QtWidgets.QSystemTrayIcon(QtGui.QIcon.fromTheme('discord-overlay-tray', QtGui.QIcon(pm)), self.app) 147 | self.trayMenu = QtWidgets.QMenu() 148 | self.showAction3 = self.trayMenu.addAction("Settings") 149 | self.showAction3.triggered.connect(self.showPresetWindow) 150 | self.exitAction = self.trayMenu.addAction("Close") 151 | self.exitAction.triggered.connect(self.exit) 152 | self.trayIcon.setContextMenu(self.trayMenu) 153 | self.trayIcon.show() 154 | 155 | def showPresetWindow(self): 156 | self.presets = QtWidgets.QMainWindow() 157 | self.presets.setWindowTitle('Configure overlays') 158 | self.fillPresetWindow() 159 | self.presets.show() 160 | 161 | def fillPresetWindow(self): 162 | gridW = QtWidgets.QWidget() 163 | grid = QtWidgets.QGridLayout() 164 | gridW.setLayout(grid) 165 | count = 0 166 | done = [] 167 | for overlay in self.overlays: 168 | if overlay.name in done: 169 | continue 170 | done.append(overlay.name) 171 | label = QtWidgets.QLabel() 172 | label.setText(overlay.name) 173 | buttonSettings = QtWidgets.QPushButton('Layout') 174 | buttonPosition = QtWidgets.QPushButton('Position') 175 | buttonDelete = QtWidgets.QPushButton('Delete') 176 | buttonSettings.clicked.connect(overlay.showSettings) 177 | buttonPosition.clicked.connect(overlay.showPosition) 178 | buttonDelete.clicked.connect(lambda: self.deleteOverlay(overlay)) 179 | grid.addWidget(label, count, 0) 180 | grid.addWidget(buttonSettings, count, 1) 181 | grid.addWidget(buttonPosition, count, 2) 182 | grid.addWidget(buttonDelete, count, 3) 183 | count = count+1 184 | addOverlayButton = QtWidgets.QPushButton('Add overlay') 185 | addOverlayButton.clicked.connect(self.addOverlay) 186 | addOverlayButton.setMaximumWidth(150) 187 | self.textbox = QtWidgets.QLineEdit() 188 | self.textbox.setMaximumWidth(150) 189 | self.textbox.setPlaceholderText('overlay Name') 190 | noneBox = QtWidgets.QLabel() 191 | noneBox.setMaximumWidth(150) 192 | grid.addWidget(noneBox, count, 3) 193 | grid.addWidget(addOverlayButton, count, 2) 194 | grid.addWidget(self.textbox, count, 1) 195 | self.presets.setCentralWidget(gridW) 196 | 197 | def addOverlay(self, button=None): 198 | if re.match('^[a-z]+$', self.textbox.text()): 199 | overlay = Overlay(self, self.configFileName, 200 | self.textbox.text(), None) 201 | overlay.load() 202 | self.overlays.append(overlay) 203 | self.fillPresetWindow() 204 | else: 205 | error_dialog = QtWidgets.QErrorMessage() 206 | error_dialog.showMessage( 207 | "Please only use lower case letters and no symbols") 208 | error_dialog.exec_() 209 | 210 | def deleteOverlay(self, overlay): 211 | n = overlay.name 212 | overlay.delete() 213 | for overlayToo in self.overlays: 214 | if overlayToo.name == n: 215 | self.overlays.remove(overlayToo) 216 | # self.overlays.remove(overlay) 217 | config = ConfigParser(interpolation=None) 218 | config.read(self.configFileName) 219 | config.remove_section(n) 220 | with open(self.configFileName, 'w') as file: 221 | config.write(file) 222 | self.fillPresetWindow() 223 | 224 | def exit(self): 225 | self.app.quit() 226 | 227 | 228 | class Overlay(QtCore.QObject): 229 | 230 | def __init__(self, up, configFileName, name, url): 231 | super().__init__() 232 | self.configFileName = configFileName 233 | self.parent = up 234 | self.app = up.app 235 | self.url = url 236 | self.size = None 237 | self.name = name 238 | self.overlay = None 239 | self.settings = None 240 | self.position = None 241 | self.enabled = True 242 | self.showtitle = True 243 | self.mutedeaf = True 244 | self.showtitle = True 245 | 246 | def load(self): 247 | config = ConfigParser(interpolation=None) 248 | config.read(self.configFileName) 249 | self.posXL = config.getint(self.name, 'xl', fallback=0) 250 | self.posXR = config.getint(self.name, 'xr', fallback=200) 251 | self.posYT = config.getint(self.name, 'yt', fallback=50) 252 | self.posYB = config.getint(self.name, 'yb', fallback=450) 253 | self.right = config.getboolean(self.name, 'rightalign', fallback=False) 254 | self.mutedeaf = config.getboolean( 255 | self.name, 'mutedeaf', fallback=True) 256 | self.chatresize = config.getboolean( 257 | self.name, 'chatresize', fallback=True) 258 | self.screenName = config.get(self.name, 'screen', fallback='None') 259 | #self.url = config.get(self.name, 'url', fallback=None) 260 | self.enabled = config.getboolean(self.name, 'enabled', fallback=True) 261 | self.showtitle = config.getboolean(self.name, 'title', fallback=True) 262 | self.hideinactive = config.getboolean( 263 | self.name, 'hideinactive', fallback=True) 264 | self.chooseScreen() 265 | # TODO Check, is there a better logic location for this? 266 | if self.enabled: 267 | self.showOverlay() 268 | 269 | def moveOverlay(self): 270 | if self.overlay: 271 | self.overlay.resize(self.posXR-self.posXL, self.posYB-self.posYT) 272 | self.overlay.move(self.posXL + self.screenOffset.left(), 273 | self.posYT + self.screenOffset.top()) 274 | 275 | def on_url(self, url): 276 | if self.overlay: 277 | self.overlay.load(QtCore.QUrl(url)) 278 | self.url = url 279 | self.save() 280 | self.settings.close() 281 | self.settings = None 282 | 283 | def on_save_position(self, url): 284 | self.save() 285 | self.position.close() 286 | self.position = None 287 | 288 | @pyqtSlot() 289 | def save(self): 290 | config = ConfigParser(interpolation=None) 291 | config.read(self.configFileName) 292 | if not config.has_section(self.name): 293 | config.add_section(self.name) 294 | config.set(self.name, 'xl', '%d' % (self.posXL)) 295 | config.set(self.name, 'xr', '%d' % (self.posXR)) 296 | config.set(self.name, 'yt', '%d' % (self.posYT)) 297 | config.set(self.name, 'yb', '%d' % (self.posYB)) 298 | config.set(self.name, 'rightalign', '%d' % (int(self.right))) 299 | config.set(self.name, 'mutedeaf', '%d' % (int(self.mutedeaf))) 300 | config.set(self.name, 'chatresize', '%d' % (int(self.chatresize))) 301 | config.set(self.name, 'screen', self.screenName) 302 | config.set(self.name, 'enabled', '%d' % (int(self.enabled))) 303 | config.set(self.name, 'title', '%d' % (int(self.showtitle))) 304 | config.set(self.name, 'hideinactive', '%d' % (int(self.hideinactive))) 305 | if self.url: 306 | config.set(self.name, 'url', self.url) 307 | if ',' in self.url: 308 | self.parent.reinit() 309 | with open(self.configFileName, 'w') as file: 310 | config.write(file) 311 | 312 | @pyqtSlot() 313 | def on_click(self): 314 | self.runJS( 315 | "document.getElementsByClassName('source-url')[0].value;", self.on_url) 316 | 317 | @pyqtSlot() 318 | def skip_stream_button(self): 319 | skipIntro = "buttons = document.getElementsByTagName('button');for(i=0;i .messages { box-sizing: border-box; width: 100%; flex: 1; }') 389 | else: 390 | self.delCSS('cssflexybox') 391 | 392 | def addCSS(self, name, css): 393 | if self.overlay: 394 | js = '(function() { css = document.getElementById(\'%s\'); if (css == null) { css = document.createElement(\'style\'); css.type=\'text/css\'; css.id=\'%s\'; document.head.appendChild(css); } css.innerText=\'%s\';})()' % (name, name, css) 395 | self.overlay.page().runJavaScript(js) 396 | 397 | def delCSS(self, name): 398 | if self.overlay: 399 | js = "(function() { css = document.getElementById('%s'); if(css!=null){ css.parentNode.removeChild(css);} })()" % ( 400 | name) 401 | self.overlay.page().runJavaScript(js) 402 | 403 | @pyqtSlot() 404 | def toggleEnabled(self, button=None): 405 | self.enabled = self.enabledButton.isChecked() 406 | if self.enabled: 407 | self.showOverlay() 408 | else: 409 | self.hideOverlay() 410 | 411 | @pyqtSlot() 412 | def toggleTitle(self, button=None): 413 | self.showtitle = self.showTitle.isChecked() 414 | if self.showtitle: 415 | self.enableShowVoiceTitle() 416 | 417 | @pyqtSlot() 418 | def toggleMuteDeaf(self, button=None): 419 | self.mutedeaf = self.muteDeaf.isChecked() 420 | if self.muteDeaf.isChecked(): 421 | self.enableMuteDeaf() 422 | 423 | @pyqtSlot() 424 | def toggleHideInactive(self, button=None): 425 | self.hideinactive = self.hideInactive.isChecked() 426 | if self.hideinactive: 427 | self.enableHideInactive() 428 | 429 | @pyqtSlot() 430 | def toggleChatResize(self, button=None): 431 | self.chatresize = self.chatResize.isChecked() 432 | self.applyTweaks() 433 | 434 | @pyqtSlot() 435 | def toggleRightAlign(self, button=None): 436 | self.right = self.rightAlign.isChecked() 437 | self.applyTweaks() 438 | 439 | @pyqtSlot() 440 | def changeValueFL(self): 441 | self.posXL = self.settingsDistanceFromLeft.value() 442 | self.moveOverlay() 443 | 444 | @pyqtSlot() 445 | def changeValueFR(self): 446 | self.posXR = self.settingsDistanceFromRight.value() 447 | self.moveOverlay() 448 | 449 | @pyqtSlot() 450 | def changeValueFT(self): 451 | self.posYT = self.settingsDistanceFromTop.value() 452 | self.moveOverlay() 453 | 454 | @pyqtSlot() 455 | def changeValueFB(self): 456 | self.posYB = self.settingsDistanceFromBottom.value() 457 | self.moveOverlay() 458 | 459 | def fillPositionWindowOptions(self): 460 | self.settingsDistanceFromLeft.valueChanged[int].connect( 461 | self.changeValueFL) 462 | self.settingsDistanceFromLeft.setMaximum(self.size.width()) 463 | self.settingsDistanceFromLeft.setValue(self.posXL) 464 | self.settingsDistanceFromRight.valueChanged[int].connect( 465 | self.changeValueFR) 466 | self.settingsDistanceFromRight.setMaximum(self.size.width()) 467 | self.settingsDistanceFromRight.setValue(self.posXR) 468 | self.settingsDistanceFromTop.valueChanged[int].connect( 469 | self.changeValueFT) 470 | self.settingsDistanceFromTop.setMaximum(self.size.height()) 471 | self.settingsDistanceFromTop.setInvertedAppearance(True) 472 | self.settingsDistanceFromTop.setValue(self.posYT) 473 | self.settingsDistanceFromBottom.valueChanged[int].connect( 474 | self.changeValueFB) 475 | self.settingsDistanceFromBottom.setMaximum(self.size.height()) 476 | self.settingsDistanceFromBottom.setInvertedAppearance(True) 477 | self.settingsDistanceFromBottom.setValue(self.posYB) 478 | 479 | def populateScreenList(self): 480 | self.ignoreScreenComboBox = True 481 | screenList = self.app.screens() 482 | self.settingsScreen.clear() 483 | for i, s in enumerate(screenList): 484 | self.settingsScreen.addItem(s.name()) 485 | if s.name() == self.screenName: 486 | self.settingsScreen.setCurrentIndex(i) 487 | 488 | self.ignoreScreenComboBox = False 489 | self.chooseScreen() 490 | 491 | def changeScreen(self, index): 492 | if not self.ignoreScreenComboBox: 493 | self.screenName = self.settingsScreen.currentText() 494 | self.chooseScreen() 495 | 496 | def chooseScreen(self): 497 | screen = None 498 | screenList = self.app.screens() 499 | logger.debug("Discovered screens: %r", [s.name() for s in screenList]) 500 | 501 | for s in screenList: 502 | if s.name() == self.screenName: 503 | screen = s 504 | logger.debug("Chose screen %s", screen.name()) 505 | break 506 | # The chosen screen is not in this list. Drop to primary 507 | else: 508 | screen = self.app.primaryScreen() 509 | logger.warning( 510 | "Chose screen %r as fallback because %r could not be matched", screen.name(), self.screenName) 511 | 512 | # Fill Info! 513 | self.size = screen.size() 514 | self.screenName = s.name() 515 | self.screenOffset = screen.availableGeometry() 516 | if self.position: 517 | self.settingsAspectRatio.updateScreen( 518 | self.size.width(), self.size.height()) 519 | self.fillPositionWindowOptions() 520 | self.screenShot(screen) 521 | self.moveOverlay() 522 | 523 | def showPosition(self): 524 | if self.position is not None: 525 | self.position.show() 526 | else: 527 | # Positional Settings Window 528 | self.position = QtWidgets.QWidget() 529 | self.position.setWindowTitle('Overlay %s Position' % (self.name)) 530 | self.positionbox = QtWidgets.QVBoxLayout() 531 | 532 | # Use a grid to lay out screen & sliders 533 | self.settingsGridWidget = QtWidgets.QWidget() 534 | self.settingsGrid = QtWidgets.QGridLayout() 535 | 536 | # Use the custom Aspect widget to keep the whole thing looking 537 | # as close to the user experience as possible 538 | self.settingsAspectRatio = AspectRatioWidget( 539 | self.settingsGridWidget) 540 | 541 | # Grid contents 542 | self.settingsPreview = ResizingImage() 543 | self.settingsPreview.setMinimumSize(1, 1) 544 | sizePolicy = QtWidgets.QSizePolicy( 545 | QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 546 | self.settingsPreview.setSizePolicy(sizePolicy) 547 | self.settingsDistanceFromLeft = QtWidgets.QSlider( 548 | QtCore.Qt.Horizontal) 549 | self.settingsDistanceFromRight = QtWidgets.QSlider( 550 | QtCore.Qt.Horizontal) 551 | self.settingsDistanceFromTop = QtWidgets.QSlider( 552 | QtCore.Qt.Vertical) 553 | self.settingsDistanceFromBottom = QtWidgets.QSlider( 554 | QtCore.Qt.Vertical) 555 | 556 | # Screen chooser 557 | self.settingsScreen = QtWidgets.QComboBox() 558 | 559 | # self.position.setMinimumSize(600,600) 560 | # Save button 561 | self.settingSave = QtWidgets.QPushButton("Save") 562 | 563 | # Fill Screens, Choose the screen if config is set 564 | self.populateScreenList() 565 | 566 | self.settingSave.clicked.connect(self.on_save_position) 567 | self.settingsScreen.currentIndexChanged.connect(self.changeScreen) 568 | 569 | self.settingsGrid.addWidget(self.settingsPreview, 0, 0) 570 | self.settingsGrid.addWidget(self.settingsDistanceFromLeft, 1, 0) 571 | self.settingsGrid.addWidget(self.settingsDistanceFromRight, 2, 0) 572 | self.settingsGrid.addWidget(self.settingsDistanceFromTop, 0, 1) 573 | self.settingsGrid.addWidget(self.settingsDistanceFromBottom, 0, 2) 574 | self.settingsGridWidget.setLayout(self.settingsGrid) 575 | self.positionbox.addWidget(self.settingsScreen) 576 | self.positionbox.addWidget(self.settingsAspectRatio) 577 | self.position.setLayout(self.positionbox) 578 | self.positionbox.addWidget(self.settingSave) 579 | self.fillPositionWindowOptions() 580 | self.position.show() 581 | 582 | def showSettings(self): 583 | if self.settings is not None: 584 | self.settings.show() 585 | else: 586 | self.settings = QtWidgets.QWidget() 587 | self.settings.setWindowTitle('Overlay %s Layout' % (self.name)) 588 | self.settingsbox = QtWidgets.QVBoxLayout() 589 | self.settingWebView = QWebEngineView() 590 | self.rightAlign = QtWidgets.QCheckBox("Right Align") 591 | self.muteDeaf = QtWidgets.QCheckBox("Show mute and deafen") 592 | self.chatResize = QtWidgets.QCheckBox("Large chat box") 593 | self.showTitle = QtWidgets.QCheckBox("Show room title") 594 | self.hideInactive = QtWidgets.QCheckBox( 595 | "Hide voice channel when inactive") 596 | self.enabledButton = QtWidgets.QCheckBox("Enabled") 597 | self.settingTakeUrl = QtWidgets.QPushButton("Use this Room") 598 | self.settingTakeAllUrl = QtWidgets.QPushButton("Use all Rooms") 599 | 600 | self.settings.setMinimumSize(400, 400) 601 | self.settingTakeUrl.clicked.connect(self.on_click) 602 | self.settingTakeAllUrl.clicked.connect(self.getAllRooms) 603 | self.settingWebView.loadFinished.connect(self.skip_stream_button) 604 | self.rightAlign.stateChanged.connect(self.toggleRightAlign) 605 | self.rightAlign.setChecked(self.right) 606 | self.muteDeaf.stateChanged.connect(self.toggleMuteDeaf) 607 | self.muteDeaf.setChecked(self.mutedeaf) 608 | self.showTitle.stateChanged.connect(self.toggleTitle) 609 | self.showTitle.setChecked(self.showtitle) 610 | self.hideInactive.stateChanged.connect(self.toggleHideInactive) 611 | self.hideInactive.setChecked(self.hideinactive) 612 | self.enabledButton.stateChanged.connect(self.toggleEnabled) 613 | self.enabledButton.setChecked(self.enabled) 614 | self.chatResize.stateChanged.connect(self.toggleChatResize) 615 | self.chatResize.setChecked(self.chatresize) 616 | 617 | self.settingWebView.load(QtCore.QUrl( 618 | "https://streamkit.discord.com/overlay")) 619 | 620 | self.settingsbox.addWidget(self.settingWebView) 621 | self.settingsbox.addWidget(self.rightAlign) 622 | self.settingsbox.addWidget(self.muteDeaf) 623 | self.settingsbox.addWidget(self.chatResize) 624 | self.settingsbox.addWidget(self.showTitle) 625 | self.settingsbox.addWidget(self.hideInactive) 626 | self.settingsbox.addWidget(self.enabledButton) 627 | self.settingsbox.addWidget(self.settingTakeUrl) 628 | self.settingsbox.addWidget(self.settingTakeAllUrl) 629 | self.settings.setLayout(self.settingsbox) 630 | self.settings.show() 631 | 632 | def screenShot(self, screen): 633 | screenshot = screen.grabWindow(0) 634 | self.settingsPreview.setImage(screenshot) 635 | self.settingsPreview.setContentsMargins(0, 0, 0, 0) 636 | 637 | def showOverlay(self): 638 | if self.overlay: 639 | return 640 | self.overlay = QWebEngineView() 641 | self.overlay.page().setBackgroundColor(QtCore.Qt.transparent) 642 | self.overlay.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) 643 | self.overlay.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True) 644 | self.overlay.setWindowFlags( 645 | QtCore.Qt.X11BypassWindowManagerHint | 646 | QtCore.Qt.FramelessWindowHint | 647 | QtCore.Qt.WindowStaysOnTopHint | 648 | QtCore.Qt.WindowTransparentForInput | 649 | QtCore.Qt.WindowDoesNotAcceptFocus | 650 | QtCore.Qt.NoDropShadowWindowHint | 651 | QtCore.Qt.WindowSystemMenuHint | 652 | QtCore.Qt.WindowMinimizeButtonHint 653 | ) 654 | self.overlay.loadFinished.connect(self.applyTweaks) 655 | self.overlay.load(QtCore.QUrl(self.url)) 656 | 657 | self.overlay.setStyleSheet("background:transparent;") 658 | self.overlay.show() 659 | self.moveOverlay() 660 | 661 | def hideOverlay(self): 662 | if self.overlay: 663 | self.overlay.close() 664 | self.overlay = None 665 | 666 | def delete(self): 667 | self.hideOverlay() 668 | if self.settings: 669 | self.settings.close() 670 | if self.position: 671 | self.position.close() 672 | self.overlay = None 673 | 674 | def getAllRooms(self): 675 | getChannel = "[window.channels, window.guilds]" 676 | self.runJS(getChannel, self.gotAllRooms) 677 | 678 | def gotAllRooms(self, message): 679 | sep = '' 680 | url = '' 681 | for chan in message[0]: 682 | if chan['type'] == 2: 683 | url += sep+"https://streamkit.discord.com/overlay/voice/%s/%s?icon=true&online=true&logo=white&text_color=%%23ffffff&text_size=14&text_outline_color=%%23000000&text_outline_size=0&text_shadow_color=%%23000000&text_shadow_size=0&bg_color=%%231e2124&bg_opacity=0.95&bg_shadow_color=%%23000000&bg_shadow_size=0&limit_speaking=false&small_avatars=false&hide_names=false&fade_chat=0" % (message[1], chan[ 684 | 'id']) 685 | sep = ',' 686 | 687 | if self.overlay: 688 | self.overlay.load(QtCore.QUrl(url)) 689 | self.url = url 690 | self.save() 691 | self.settings.close() 692 | self.settings = None 693 | 694 | 695 | def entrypoint(): 696 | def main(app): 697 | logging.basicConfig( 698 | format='%(asctime)-15s %(levelname)-8s %(message)s') 699 | try: 700 | logging.getLogger().setLevel(os.environ.get("LOGLEVEL", "DEBUG").upper()) 701 | except ValueError as exc: 702 | logger.warning("Can't set log level: %s", exc) 703 | 704 | a = App(app) 705 | a.main() 706 | app.exec() 707 | 708 | app = QtWidgets.QApplication(sys.argv) 709 | main(app) 710 | --------------------------------------------------------------------------------