├── 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 | 
8 | 
9 | 
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 |
--------------------------------------------------------------------------------