├── .gitignore ├── requirements.txt ├── main.py ├── Observer.py ├── README.md ├── webhook.py ├── MatchHistory.py ├── UIListener.py └── MainWindow.py /.gitignore: -------------------------------------------------------------------------------- 1 | /src 2 | /target 3 | /venv 4 | /__pycache__ 5 | matches.json 6 | notes.json -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5==5.9.2 2 | PyInstaller==3.4 3 | qdarkstyle==2.6.5 4 | requests==2.21.0 -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys, qdarkstyle 2 | from PyQt5.QtWidgets import QApplication 3 | from MainWindow import MainWindow 4 | 5 | def main(): 6 | app = QApplication(sys.argv) 7 | app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 8 | mw = MainWindow() 9 | sys.exit(app.exec_()) 10 | 11 | if __name__ == "__main__": 12 | main() -------------------------------------------------------------------------------- /Observer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject 2 | 3 | class Observer(QObject): 4 | def __init__(self): 5 | QObject.__init__(self) 6 | 7 | def notify(self, data): 8 | pass 9 | 10 | # data = { 11 | # "players": [ 12 | # { 13 | # "name": "playerA", 14 | # "type": "user", 15 | # "race": "Prot", 16 | # "result": "Defeat", 17 | # "isme": "true" 18 | # }, 19 | # { 20 | # "name": "playerB", 21 | # "type": "computer", 22 | # "race": "random", 23 | # "result": "Victory", 24 | # "isme": "false" 25 | # } 26 | # ], 27 | # "displayTime": "5.000000", 28 | # "event": "enter" 29 | # } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SC2Notes 2 | ### [Download](https://github.com/leigholiver/sc2notes/releases/latest) 3 | 4 | ##### Usage 5 | Download and run `sc2notes.exe` 6 | Copy the `SC2Switcher URL` into the Webhook tab of the [SC2Switcher obs-studio plugin](https://github.com/leigholiver/OBS-SC2Switcher) or [SC2Switcher Standalone](https://github.com/leigholiver/SC2Switcher-Standalone) 7 | 8 | Make sure that 'Webhook enabled' is ticked in SC2Switcher and you have entered your username into the Usernames tab 9 | 10 | When a game starts your notes and match history against your opponent should be displayed. 11 | 12 | Your notes and match history are stored in `notes.json` and `matches.json` respectively if you want to back them up. 13 | 14 | ##### Build instructions 15 | `pip install -r requirements.txt` 16 | 17 | `pyinstaller main.py --noconsole --name sc2notes` -------------------------------------------------------------------------------- /webhook.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | from urllib.parse import urlparse, parse_qs 3 | from PyQt5.QtCore import QThread 4 | import json 5 | 6 | def makeHandlerClass(child): 7 | class CustomHandler(reqHandler, object): 8 | def __init__(self, *args, **kwargs): 9 | self.child = child 10 | super(CustomHandler, self).__init__(*args, **kwargs) 11 | return CustomHandler 12 | 13 | class reqHandler(BaseHTTPRequestHandler): 14 | def do_GET(self): 15 | req = parse_qs(urlparse(self.path).query) 16 | data = json.loads(req['json'][0]) 17 | self.child.notify(data) 18 | self.send_response(200) 19 | self.end_headers() 20 | return 21 | 22 | class httpListener(QThread): 23 | def __init__(self, port): 24 | QThread.__init__(self) 25 | self.observers = set() 26 | self.port = port 27 | 28 | def run(self): 29 | handlerclass = makeHandlerClass(self) 30 | self.httpserver = HTTPServer(('', self.port), handlerclass) 31 | sa = self.httpserver.socket.getsockname() 32 | print('Listening on ' + str(sa[0]) + ':' + str(sa[1])) 33 | self.httpserver.serve_forever() 34 | 35 | def stop(self): 36 | self.httpserver.shutdown() 37 | self.httpserver.socket.close() 38 | self.wait() 39 | 40 | def notify(self, data): 41 | for obs in self.observers: 42 | obs.notify(data) 43 | 44 | def attach(self, obs): 45 | self.observers.add(obs) -------------------------------------------------------------------------------- /MatchHistory.py: -------------------------------------------------------------------------------- 1 | import json, datetime 2 | from Observer import Observer 3 | 4 | class MatchHistory(Observer): 5 | matches = {} 6 | recents = [] 7 | 8 | def notify(self, data): 9 | if data['event'] == 'exit' and len(data['players']) == 2: 10 | found = False 11 | opFound = False 12 | result = "" 13 | op = None 14 | for p in data['players']: 15 | if p['isme']: 16 | # if we already found us, we're probably same name? 17 | if found: 18 | opFound = True 19 | op = p 20 | result = "Unknown" 21 | else: 22 | found = True 23 | result = p['result'] 24 | elif p['isme'] == False and opFound == False: 25 | opFound = True 26 | op = p 27 | 28 | if found and opFound: 29 | m = {} 30 | m['opponent'] = op['name'] 31 | m['race'] = op['race'] 32 | m['gametime'] = data['displayTime'] 33 | m['result'] = result 34 | m['date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") 35 | 36 | if op['name'] in self.matches: 37 | self.matches[op['name']].append(m) 38 | else: 39 | self.matches[op['name']] = [ m ] 40 | 41 | with open('matches.json', 'w+') as outfile: 42 | json.dump(self.matches, outfile) 43 | 44 | self.recents.append(m) 45 | print(self.recents) 46 | 47 | def __init__(self): 48 | Observer.__init__(self) 49 | try: 50 | with open('matches.json', 'r') as infile: 51 | m = json.load(infile) 52 | self.matches = m 53 | except: 54 | # exception if file doesnt exist, we dont need to 55 | # create it now - matches dict is already created 56 | # and file will get created on first write 57 | pass -------------------------------------------------------------------------------- /UIListener.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PyQt5.QtCore import pyqtSignal 3 | from Observer import Observer 4 | 5 | class UIListener(Observer): 6 | loadText = pyqtSignal(str) 7 | updateRecents = pyqtSignal() 8 | 9 | def notify(self, data): 10 | if len(data['players']) == 2: 11 | found = False 12 | opFound = False 13 | op = None 14 | result = "" 15 | for p in data['players']: 16 | if p['isme']: 17 | # if we already found us, we're probably same name? 18 | if found: 19 | opFound = True 20 | op = p 21 | result = "Unknown Result" 22 | found = True 23 | result = p['result'] 24 | elif p['isme'] == False and opFound == False: 25 | opFound = True 26 | op = p 27 | 28 | if found and opFound and op != None: 29 | if data['event'] == 'enter': 30 | self.ui.OPName = op['name'] + "/" + op['race'] 31 | if op['name'] + "/" + op['race'] in self.ui.notes: 32 | self.loadText.emit(self.ui.notes[op['name'] + "/" + op['race']]) 33 | elif op['name'] in self.ui.notes: 34 | self.loadText.emit(self.ui.notes[op['name']]) 35 | else: 36 | self.loadText.emit("") 37 | 38 | wins = 0 39 | games = 0 40 | self.ui.historyLabel.setText("") 41 | if op['name'] in self.mh.matches: 42 | games = len(self.mh.matches[op['name']]) 43 | out = "" 44 | for i in reversed(range(len(self.mh.matches[op['name']]))): 45 | m = self.mh.matches[op['name']][i] 46 | secs = float(m['gametime']) 47 | mins = secs / 60 48 | secs = secs % 60 49 | out += m['result'] + " - " + m['race'] + " - " + str(int(mins)) + "m" + str(int(secs)) + "s - " +m['date'] + "\n" 50 | if m['result'] == 'Victory': wins = wins + 1 51 | self.ui.historyLabel.setText(out) 52 | 53 | if games == 0: 54 | winrate = "0" 55 | else: 56 | winrate = str(int((wins/games) * 100)) 57 | 58 | self.ui.gameLabel.setText("In game against " + op['name'] + " (" + op['race'] + ") - " + 59 | str(wins) + ":" + str(games - wins) + " (" + winrate + "%)") 60 | elif data['event'] == 'exit': 61 | self.ui.gameLabel.setText(result + " against " + op['name'] + " (" + op['race'] + ")") 62 | self.updateRecents.emit(); 63 | else: 64 | self.ui.gameLabel.setText(" ") 65 | 66 | def __init__(self, ui, mh): 67 | Observer.__init__(self) 68 | self.ui = ui 69 | self.mh = mh -------------------------------------------------------------------------------- /MainWindow.py: -------------------------------------------------------------------------------- 1 | import json, requests 2 | from functools import partial 3 | from PyQt5.QtWidgets import QWidget, QTextEdit, QHBoxLayout, QLabel, QVBoxLayout, QLineEdit, QFormLayout, QPushButton, QScrollArea, QSlider 4 | from PyQt5.QtGui import QIcon, QFont 5 | from PyQt5.QtCore import pyqtSlot, Qt, QSettings, QSize 6 | from webhook import httpListener 7 | from MatchHistory import MatchHistory 8 | from UIListener import UIListener 9 | 10 | class MainWindow(QWidget): 11 | notes = {} 12 | OPName = "" 13 | 14 | def __init__(self): 15 | QWidget.__init__(self) 16 | try: 17 | with open('notes.json', 'r') as infile: 18 | m = json.load(infile) 19 | self.notes = m 20 | except: 21 | pass 22 | 23 | self.hl = httpListener(8081) 24 | self.mh = MatchHistory() 25 | self.ui = UIListener(self, self.mh) 26 | self.ui.loadText.connect(self.updateText) 27 | self.ui.updateRecents.connect(self.loadRecents) 28 | self.hl.attach(self.mh) 29 | self.hl.attach(self.ui) 30 | self.hl.start() 31 | 32 | # window 33 | self.setWindowTitle('SC2Notes') 34 | self.settings = QSettings('leigholiver', 'sc2notes') 35 | self.resize(self.settings.value("size", QSize(900, 450))) 36 | if self.settings.value("pos") != None: 37 | self.move(self.settings.value("pos")) 38 | 39 | # layout 40 | layout = QHBoxLayout() 41 | layout.setAlignment(Qt.AlignTop) 42 | 43 | col1 = QVBoxLayout() 44 | col1.setAlignment(Qt.AlignTop) 45 | col1.addWidget(QLabel("Notes")) 46 | 47 | self.noteText = QTextEdit() 48 | self.noteText.textChanged.connect(lambda: self.saveNoteText(self.noteText.document().toPlainText())) 49 | 50 | col1.addWidget(self.noteText) 51 | 52 | col1.addWidget(QLabel("Font Size")) 53 | self.fontSize = QSlider(Qt.Horizontal) 54 | self.fontSize.setMinimum(9) 55 | self.fontSize.setMaximum(48) 56 | if self.settings.value("fontSize") != None: 57 | self.fontSize.setValue(int(self.settings.value("fontSize"))) 58 | self.setFontSize() 59 | self.fontSize.valueChanged.connect(self.setFontSize) 60 | 61 | col1.addWidget(self.fontSize) 62 | layout.addLayout(col1, 3) 63 | 64 | col2 = QVBoxLayout() 65 | col2.setAlignment(Qt.AlignTop) 66 | 67 | col2.addWidget(QLabel("")) # spacer 68 | 69 | # check for updates 70 | self.updates = QVBoxLayout() 71 | self.updates.setAlignment(Qt.AlignTop) 72 | self.updates.setContentsMargins(0, 0, 0, 0) 73 | col2.addLayout(self.updates) 74 | self.checkForUpdates() 75 | 76 | ipaddrForm = QHBoxLayout() 77 | ipaddrLabel = QLabel("SC2Switcher URL:") 78 | ipaddrBox = QLineEdit("http://localhost:8081/") 79 | ipaddrForm.addWidget(ipaddrLabel) 80 | ipaddrForm.addWidget(ipaddrBox) 81 | col2.addLayout(ipaddrForm) 82 | 83 | searchLabel = QLabel("Search:") 84 | self.searchBox = QLineEdit() 85 | self.searchBox.returnPressed.connect(lambda: self.doSearch(self.searchBox.text())) 86 | searchForm = QHBoxLayout() 87 | searchbtn = QPushButton("Go") 88 | searchbtn.clicked.connect(lambda: self.doSearch(self.searchBox.text())) 89 | searchForm.addWidget(searchLabel) 90 | searchForm.addWidget(self.searchBox) 91 | searchForm.addWidget(searchbtn) 92 | col2.addLayout(searchForm) 93 | 94 | self.searchResults = QWidget() 95 | tmplayout = QVBoxLayout() 96 | tmplayout.setAlignment(Qt.AlignTop) 97 | tmplayout.setContentsMargins(0, 0, 0, 0) 98 | self.searchResults.setLayout(tmplayout) 99 | 100 | self.scroll = QScrollArea() 101 | self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 102 | self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 103 | self.scroll.setWidgetResizable(True) 104 | self.scroll.setWidget(self.searchResults) 105 | 106 | scroll_layout = QVBoxLayout() 107 | scroll_layout.setAlignment(Qt.AlignTop) 108 | scroll_layout.addWidget(self.scroll) 109 | scroll_layout.setContentsMargins(0, 0, 0, 0) 110 | col2.addWidget(self.scroll) 111 | self.scroll.hide() 112 | 113 | self.gameLabel = QLabel("") 114 | col2.addWidget(self.gameLabel) 115 | 116 | col2.addWidget(QLabel("Match History")) 117 | 118 | 119 | self.historyLabel = QLabel() 120 | widget = QWidget() 121 | tmplayout = QVBoxLayout() 122 | tmplayout.setAlignment(Qt.AlignTop) 123 | tmplayout.setContentsMargins(0, 0, 0, 0) 124 | tmplayout.addWidget(self.historyLabel) 125 | widget.setLayout(tmplayout) 126 | 127 | self.historyLabelScroll = QScrollArea() 128 | self.historyLabelScroll.setAlignment(Qt.AlignTop) 129 | self.historyLabelScroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 130 | self.historyLabelScroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 131 | self.historyLabelScroll.setWidgetResizable(True) 132 | self.historyLabelScroll.setWidget(widget) 133 | 134 | scroll_layout = QVBoxLayout() 135 | scroll_layout.setAlignment(Qt.AlignTop) 136 | scroll_layout.addWidget(self.historyLabelScroll) 137 | scroll_layout.setContentsMargins(0, 0, 0, 0) 138 | col2.addWidget(self.historyLabelScroll) 139 | 140 | self.recentMatchesText = QLabel("Recent Matches") 141 | col2.addWidget(self.recentMatchesText) 142 | 143 | self.recentMatches = QWidget() 144 | tmplayout = QVBoxLayout() 145 | tmplayout.setAlignment(Qt.AlignTop) 146 | tmplayout.setContentsMargins(0, 0, 0, 0) 147 | self.recentMatches.setLayout(tmplayout) 148 | 149 | self.recentMatchesScroll = QScrollArea() 150 | self.recentMatchesScroll.setAlignment(Qt.AlignTop) 151 | self.recentMatchesScroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 152 | self.recentMatchesScroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 153 | self.recentMatchesScroll.setWidgetResizable(True) 154 | self.recentMatchesScroll.setWidget(self.recentMatches) 155 | 156 | scroll_layout = QVBoxLayout() 157 | scroll_layout.setAlignment(Qt.AlignTop) 158 | scroll_layout.addWidget(self.recentMatchesScroll) 159 | scroll_layout.setContentsMargins(0, 0, 0, 0) 160 | col2.addWidget(self.recentMatchesScroll) 161 | 162 | layout.addLayout(col2, 2) 163 | 164 | self.setLayout(layout) 165 | self.show() 166 | 167 | def saveNoteText(self, text): 168 | self.notes[self.OPName] = text 169 | with open('notes.json', 'w') as outfile: 170 | json.dump(self.notes, outfile) 171 | 172 | def closeEvent(self, e): 173 | self.settings.setValue("size", self.size()) 174 | self.settings.setValue("pos", self.pos()) 175 | e.accept() 176 | 177 | def updateText(self, text): 178 | self.noteText.setText(text) 179 | 180 | def doSearch(self, query): 181 | layout = self.searchResults.layout() 182 | index = layout.count() 183 | while(index >= 0): 184 | if layout.itemAt(index) != None: 185 | myWidget = layout.itemAt(index).widget() 186 | myWidget.setParent(None) 187 | index -=1 188 | 189 | self.gameLabel.setText(""); 190 | results = [] 191 | for name in self.notes.keys(): 192 | if query in name and name not in results: 193 | results.append(name) 194 | if query in self.notes[name] and name not in results: 195 | results.append(name) 196 | for name in self.mh.matches.keys(): 197 | if query in name and name not in results: 198 | results.append(name) 199 | 200 | if len(results) > 0: 201 | for name in results: 202 | if name != "": 203 | btn = QPushButton(name) 204 | btn.clicked.connect(partial(self.loadFromUsername, name)) 205 | layout.addWidget(btn) 206 | 207 | self.scroll.show() 208 | else: 209 | self.gameLabel.setText("No results found."); 210 | 211 | def loadFromUsername(self, username): 212 | if username in self.notes: 213 | self.OPName = username 214 | self.noteText.setText(self.notes[username]) 215 | if (username in self.mh.matches) == False: 216 | self.historyLabel.setText("") 217 | 218 | if username in self.mh.matches: 219 | out = "" 220 | wins = 0 221 | games = 0 222 | for i in reversed(range(len(self.mh.matches[username]))): 223 | games = len(self.mh.matches[username]) 224 | m = self.mh.matches[username][i] 225 | secs = float(m['gametime']) 226 | mins = secs / 60 227 | secs = secs % 60 228 | out += m['result'] + " - " + m['race'] + " - " + str(int(mins)) + "m" + str(int(secs)) + "s - " + m['date'] + "\n" 229 | if m['result'] == 'Victory': wins = wins + 1 230 | self.OPName = username 231 | self.historyLabel.setText(out) 232 | winrate = "0" 233 | if games > 0: 234 | winrate = str(int((wins/games) * 100)) 235 | self.gameLabel.setText("History against " + username + " - " + 236 | str(wins) + ":" + str(games - wins) + " (" + winrate + "%)") 237 | if (username in self.notes) == False: 238 | self.noteText.setText("") 239 | self.scroll.hide() 240 | 241 | def loadRecents(self): 242 | layout = self.recentMatches.layout() 243 | index = layout.count() 244 | while(index >= 0): 245 | if layout.itemAt(index) != None: 246 | myWidget = layout.itemAt(index).widget() 247 | myWidget.setParent(None) 248 | index -=1 249 | wins = 0 250 | games = 0 251 | for i in reversed(range(len(self.mh.recents))): 252 | games = len(self.mh.recents) 253 | m = self.mh.recents[i] 254 | secs = float(m['gametime']) 255 | mins = secs / 60 256 | secs = secs % 60 257 | btn = QPushButton(m['result'] + " - " + m['opponent'] + " - " + m['race'] + " - " + str(int(mins)) + "m" + str(int(secs)) + "s - " + m['date']) 258 | btn.clicked.connect(partial(self.loadFromUsername, m['opponent'])) 259 | layout.addWidget(btn) 260 | if m['result'] == 'Victory': wins = wins + 1 261 | winrate = "0" 262 | if games > 0: 263 | winrate = str(int((wins/games) * 100)) 264 | self.recentMatchesText.setText("Recent Matches - " + 265 | str(wins) + ":" + str(games - wins) + " (" + winrate + "%)") 266 | 267 | def setFontSize(self): 268 | font = QFont() 269 | font.setPointSize(self.fontSize.value()) 270 | self.noteText.setFont(font) 271 | self.settings.setValue("fontSize", self.fontSize.value()) 272 | 273 | def checkForUpdates(self): 274 | r = requests.get('https://api.github.com/repos/leigholiver/sc2notes/releases/latest') 275 | response = r.json() 276 | if float(response['tag_name']) > 0.4: 277 | updateLink = QLabel("Update Available. Click here to download.") 278 | updateLink.setTextFormat(Qt.RichText) 279 | updateLink.setTextInteractionFlags(Qt.TextBrowserInteraction) 280 | updateLink.setOpenExternalLinks(True) 281 | self.updates.addWidget(updateLink) 282 | self.updates.addWidget(QLabel(response['body'])) --------------------------------------------------------------------------------