From c600ec17838785cd5b75eae9421160a6747431c3 Mon Sep 17 00:00:00 2001
From: Phoenix/HotaruBlaze
Date: Mon, 31 Aug 2026 15:17:51 +0100
Subject: [PATCH 1/2] Migrate to API key auth with endpoint selection, Client
v0.32
---
api/util.py | 59 ++++-
app.py | 287 +++++++++++++++-----
client.py | 228 +++++++++++-----
layout/__init__.py | 608 +++++++++++++++++++++++++------------------
layout/mainwindow.ui | 573 +++++++++++++++++++++++++++++++---------
5 files changed, 1224 insertions(+), 531 deletions(-)
diff --git a/api/util.py b/api/util.py
index 493fa11..83e3f15 100644
--- a/api/util.py
+++ b/api/util.py
@@ -218,17 +218,22 @@ def status(self):
"""
Shows your current status
"""
+ console = self.client.userData['consoles'][0] if self.client.userData.get('consoles') else None
text = [
'%s: %s' % ('Exception', self.client.userData['Exception']),
- '%s: %s' % ('Friend Code', self.client.userData['User']['friendCode']),
- '%s: %s' % ('Online', self.client.userData['User']['online']),
- '%s: %s' % ('Message', self.client.userData['User']['message']),
]
- if self.client.userData['User']['username']:
- text.append('%s: %s' % ('Username', self.client.userData['User']['username']))
- text.append('%s: %s' % ('Mii', self.client.userData['User']['mii']['face']))
- if self.client.userData['User']['online']:
- text.append('%s: %s' % ('Game', self.client.userData['User']['Presence']['game']['name']))
+ if not console:
+ text.append('%s: %s' % ('Online', False))
+ return self._log('\n'.join(text), Color.BLUE)
+ text.append('%s: %s' % ('Friend Code', console['friendCode']))
+ text.append('%s: %s' % ('Network', console['network']))
+ text.append('%s: %s' % ('Online', console['online']))
+ text.append('%s: %s' % ('Message', console['message']))
+ if console.get('username'):
+ text.append('%s: %s' % ('Username', console['username']))
+ text.append('%s: %s' % ('Mii', console['mii']['face']))
+ if console['online'] and console.get('Presence'):
+ text.append('%s: %s' % ('Game', console['Presence']['game']['name']))
return self._log('\n'.join(text), Color.BLUE)
def discord(self, command:typing.Literal['connect', 'disconnect'] = 'connect', pipe:int = '0'):
@@ -291,6 +296,21 @@ def config(self, command:typing.Literal['help', 'profilebutton', 'elapsedtime',
self.client.reflectConfig()
return self._log('Done', Color.BLUE)
+ def apikey(self, newKey:str):
+ """
+ Sets a new API key
+ self, newKey:str
+ """
+ self.client.updateKey(newKey)
+ return self._log('Done', Color.BLUE)
+
+ def refresh(self):
+ """
+ Manually fetches the latest status from the server
+ """
+ self.client.loop()
+ return self._log('Done', Color.BLUE)
+
def log(self):
"""
Shows activity log
@@ -299,12 +319,23 @@ def log(self):
# Exception handling
def APIExcept(r):
- text = r.text
- if '429' in r.text:
- text = 'You have reached your rate-limit for this resource.'
- elif '502' in r.text:
- text = 'The frontend is offline. Please try again later.'
- raise APIException(text)
+ status = r.status_code
+ if status == 429:
+ raise APIException('You have reached your rate-limit for this resource.')
+ if status == 404:
+ raise APIException('Not found (404): the configured endpoint does not expose this API route. Make sure you selected the correct endpoint.')
+ if status == 502:
+ raise APIException('Backend currently offline. please try again later')
+ try:
+ data = r.json()
+ text = str(data.get('Exception') or data)
+ except Exception:
+ text = r.text
+ if ' 200:
+ text = text[:200] + '...'
+ raise APIException(text or 'Unexpected response from the server')
class APIException(Exception):
pass
diff --git a/app.py b/app.py
index 4182479..aef1b8f 100644
--- a/app.py
+++ b/app.py
@@ -8,18 +8,42 @@
from client import _REGION
import webbrowser
+import time
+
+from enum import IntEnum
+
+class Page(IntEnum):
+ WELCOME = 0
+ ENDPOINT = 1
+ LINK = 2
+ LOADING = 3
+ MAIN = 4
+ SETTINGS = 5
+
+class UIBridge(QObject):
+ updated = pyqtSignal(object)
+ errored = pyqtSignal(object, object)
+ reauthRequested = pyqtSignal()
+ statusChanged = pyqtSignal(str)
+
+def fmtTime(ts):
+ if not ts:
+ return '\u2014'
+ try:
+ return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(ts)))
+ except (ValueError, TypeError, OverflowError):
+ return str(ts)
# 3DS Variables
-friendCode = None
+apiKey = ''
config = {}
if not os.path.isdir(path):
os.mkdir(path)
if os.path.isfile(privateFile):
with open(privateFile, 'r') as file:
js = json.loads(file.read())
- friendCode = js['friendCode']
+ apiKey = js.get('apiKey', '')
config = js
- del config['friendCode']
client = None
# PyQt5 Variables
@@ -29,7 +53,10 @@
def loadPix(url):
_pixmap = QPixmap()
- _pixmap.loadFromData(requests.get(url, verify = False).content)
+ try:
+ _pixmap.loadFromData(requests.get(url, verify = False, timeout = 10).content)
+ except Exception:
+ pass
return _pixmap
def up(_label,image):
@@ -49,46 +76,116 @@ def __init__(self, MainWindow):
self.underLyingButton2.clicked.connect(lambda a : self.errorMes())
self.err = None
self.traceback = None
+ self._customEndpoint = ''
+ self.reauthing = False
+ self.styled = False
+ self.bridge = UIBridge()
+ self.bridge.updated.connect(self.update)
+ self.bridge.errored.connect(self.error)
+ self.bridge.reauthRequested.connect(self.reauthorize)
+ self.bridge.statusChanged.connect(self.setStatus)
def selfService(self, app):
self.app = app
self.MainWindow.setStyleSheet(style)
self.assignVariables()
- self.page = 0
+ self.versionLabel.setText('3DS-RPC v%s' % version)
+ self.page = Page.WELCOME
self.updatePage()
self.MainWindow.closeEvent = self.closeEvent
self.underLyingButton = QPushButton()
self.underLyingButton.clicked.connect(lambda a : self.updateColor())
- if self.state and client.userData.get('User'):
+ if self.state and client.userData.get('consoles'):
self.stylize()
self.closeButton.clicked.connect(sys.exit)
- self.settingsButton.clicked.connect(lambda a : self.updatePage(3))
- self.okButton.clicked.connect(lambda a : self.updatePage(2))
+ self.settingsButton.clicked.connect(lambda a : self.updatePage(Page.SETTINGS))
+ self.okButton.clicked.connect(lambda a : self.updatePage(Page.MAIN))
+ self.refreshButton.clicked.connect(self.refresh)
self.state = False
- threading.Thread(target = self.grabCode, daemon = True).start()
def assignVariables(self):
- self.continueButton.clicked.connect(lambda a : self.updatePage(1))
+ self.continueButton.clicked.connect(lambda a : self.updatePage(Page.ENDPOINT))
self.loginButton.clicked.connect(self.changeState)
- self.botFCLabel.setText('-'.join(nintendoBotFC[i:i+4] for i in range(0, len(nintendoBotFC), 4)))
+ self.welcomeLogo.setPixmap(QPixmap(getPath('layout/resources/logo.png')))
+
+ self.endpointButton.clicked.connect(lambda a : self.showEndpointPage())
+ self.backEndpointButton.clicked.connect(lambda a : self.updatePage(Page.WELCOME if not client else Page.SETTINGS))
+ self.saveEndpointButton.clicked.connect(self.saveEndpoint)
+ self.endpointInput.currentIndexChanged.connect(lambda a : self.updateEndpointField())
+ self.customEndpointInput.textChanged.connect(self.captureCustomEndpoint)
+ self.endpointHint.setStyleSheet('color: #D32F2F;')
+ self.updateEndpointField()
+
+ def captureCustomEndpoint(self, text):
+ if self.customEndpointInput.isEnabled():
+ self._customEndpoint = text
+ self.updateSaveState()
+
+ def updateEndpointField(self):
+ endpoint = self.endpointInput.currentText().strip()
+ if endpoint == 'custom':
+ self.customEndpointInput.setEnabled(True)
+ self.customEndpointInput.setText(self._customEndpoint or (client.customEndpoint if client else config.get('customEndpoint', '')))
+ else:
+ self.customEndpointInput.setEnabled(False)
+ self.customEndpointInput.setText(getHost(endpoint))
+ self.updateSaveState()
+
+ def updateSaveState(self):
+ endpoint = self.endpointInput.currentText().strip()
+ if endpoint == 'custom':
+ try:
+ validateEndpoint(self.customEndpointInput.text())
+ valid, message = True, ''
+ except ValueError as e:
+ valid, message = False, str(e)
+ else:
+ valid, message = True, ''
+ self.saveEndpointButton.setEnabled(valid)
+ self.endpointHint.setText(message)
+ self.endpointHint.setVisible(endpoint == 'custom' and bool(message))
+
+ def showEndpointPage(self):
+ if not client:
+ return
+ index = self.endpointInput.findText(client.endpoint)
+ if index != -1:
+ self.endpointInput.setCurrentIndex(index)
+ self.updateEndpointField()
+ self.updatePage(Page.ENDPOINT)
+
+ def saveEndpoint(self):
+ global config
+ config['endpoint'] = self.endpointInput.currentText().strip() or 'official'
+ config['customEndpoint'] = ''
+ if config['endpoint'] == 'custom':
+ try:
+ config['customEndpoint'] = validateEndpoint(self.customEndpointInput.text())
+ except ValueError as e:
+ dlg = QMessageBox()
+ dlg.setWindowTitle('3DS-RPC')
+ dlg.setText(str(e))
+ dlg.exec_()
+ return
+ if client:
+ client.setEndpoint(config['endpoint'], config['customEndpoint'])
+ self.updatePage(Page.SETTINGS)
+ else:
+ self.updatePage(Page.LINK)
def stylize(self):
+ self.styled = True
self.underLyingButton.click()
- # Update dynamic elements
- self.setFontText(self.username, client.userData['User']['username'])
- self.miiLabel.setScaledContents(True)
- self.gameIcon.setScaledContents(True)
- if client.userData['User']['mii']:
- up(self.miiLabel, client.userData['User']['mii']['face'])
+ self.updateProfile()
+ self.updateDetails()
# Update others
- self.friendCard.mouseReleaseEvent = lambda event : self.openLink(host + '/user/%s' % client.userData['User']['friendCode'])
self.okButtonLogout.clicked.connect(self.logout)
for button in ((self.showElapsedOff, self.showElapsedOn, 'showElapsed'), (self.showProfileButtonOff, self.showProfileButtonOn, 'showProfileButton'), (self.showSmallImageOff, self.showSmallImageOn, 'showSmallImage')):
self.setFontText(button[0], 'No')
@@ -99,6 +196,30 @@ def stylize(self):
for label in (self.showElapsedText, self.showProfileButtonText, self.showSmallImageText):
self.setFontText(label, label.text())
+ def updateProfile(self):
+ console = client.userData['consoles'][0] if client.userData.get('consoles') else None
+ if not console:
+ return
+ self.miiLabel.setScaledContents(True)
+ self.gameIcon.setScaledContents(True)
+ self.setFontText(self.username, console.get('username', ''))
+ if console.get('mii'):
+ up(self.miiLabel, console['mii']['face'])
+ self.friendCard.mouseReleaseEvent = lambda event : self.openLink(client.host + '/user/%s' % console['friendCode'])
+
+ def refresh(self):
+ if not client:
+ return
+ try:
+ client.loop()
+ except Exception as e:
+ dlg = QMessageBox()
+ dlg.setWindowTitle('3DS-RPC')
+ dlg.setText('Refresh failed: %s' % str(e))
+ dlg.exec_()
+ return
+ self.updateProfile()
+
def updateSettings(self, button, activate):
client.__dict__[button[2]] = activate
client.reflectConfig()
@@ -107,18 +228,20 @@ def updateSettings(self, button, activate):
def updateColor(self):
self.MainWindow.setStyleSheet('')
- if not client.userData['User']['online']:
+ console = client.userData['consoles'][0] if client.userData.get('consoles') else None
+ online = bool(console and console.get('online'))
+ if not online:
self.MainWindow.setStyleSheet(offlineStyle)
else:
self.MainWindow.setStyleSheet(style)
- self.status.setText('Online' if client.userData['User']['online'] else 'Offline')
+ self.status.setText('Online' if online else 'Offline')
def setFontText(self, label, text):
label.setText(text)
i = 21
width = 30000
- while label.width() < width:
+ while label.width() < width and i > 1:
i -= 1
font = QFont('Arial', i)
label.setFont(font)
@@ -129,64 +252,78 @@ def openLink(self, url:str):
webbrowser.open(url)
def closeEvent(self, event):
- if friendCode and client:
+ if apiKey and client:
event.ignore()
self.MainWindow.hide()
tray.show()
- elif friendCode:
+ elif apiKey:
event.accept()
self.app.quit()
else:
sys.exit()
- def grabCode(self):
- global friendCode
- if friendCode:
- return
- friendCode = str(principal_id_to_friend_code(friend_code_to_principal_id(self.waitUntil()))).zfill(12)
-
- def waitUntil(self):
- while True:
- while not self.state:
- pass
- try:
- friend_code_to_principal_id(self.fcInput.text().strip())
- break
- except:
- self.state = False
- return self.fcInput.text().strip()
-
def changeState(self):
- self.state = True
- while not friendCode:
- if not self.state:
- dlg = QMessageBox()
- dlg.setWindowTitle('3DS-RPC')
- dlg.setText('An invalid friendcode has been passed')
- dlg.exec_()
- return
- self.MainWindow.close()
+ global apiKey
+ newKey = self.keyInput.text().strip()
+ if not newKey:
+ dlg = QMessageBox()
+ dlg.setWindowTitle('3DS-RPC')
+ dlg.setText('Please enter your API key.')
+ dlg.exec_()
+ return
+ apiKey = newKey
+ if client:
+ client.updateKey(newKey)
+ self.reauthing = False
+ self.refresh()
+ self.updatePage(Page.MAIN)
+ else:
+ self.MainWindow.close()
def updatePage(self, page = None):
self.page = page if page != None else self.page
self.stackedWidget.setCurrentIndex(page if page != None else self.page)
+ def updateDetails(self):
+ if not client:
+ return
+ self.endpointDetail.setText('Endpoint: %s' % client.host)
+ console = client.userData['consoles'][0] if client.userData.get('consoles') else None
+ if not console:
+ self.networkDetail.setText('Network: \u2014')
+ self.lastActiveDetail.setText('Last Active: \u2014')
+ self.lastOnlineDetail.setText('Last Online: \u2014')
+ return
+ self.networkDetail.setText('Network: %s' % console.get('network', '\u2014').title())
+ self.lastActiveDetail.setText('Last Active: %s' % fmtTime(console.get('lastAccessed')))
+ self.lastOnlineDetail.setText('Last Online: %s' % fmtTime(console.get('lastOnline')))
+
def update(self, data):
- if data:
- self.gamePlate.show()
- game = client.userData['User']['Presence']['game']
- self.gamePlate.mouseReleaseEvent = lambda a : self.openLink('https://www.google.com/search?q=%s' % '+'.join((game['name'] + ' ' + game['publisher']['name']).split(' ')))
- if data.get('large_image') and not local:
- up(self.gameIcon, data['large_image'])
- self.setFontText(self.gameName, data['details'])
- else:
- self.gamePlate.hide()
- self.underLyingButton.click()
+ try:
+ if self.page == Page.LOADING:
+ self.updatePage(Page.MAIN)
+ if client.userData.get('consoles') and not self.styled:
+ self.stylize()
+ self.updateProfile()
+ self.updateDetails()
+ if data:
+ self.gamePlate.show()
+ game = client.userData['consoles'][0]['Presence']['game']
+ publisher = (game.get('publisher') or {}).get('name', '')
+ self.gamePlate.mouseReleaseEvent = lambda a : self.openLink('https://www.google.com/search?q=%s' % '+'.join((game['name'] + ' ' + publisher).split(' ')))
+ if data.get('large_image') and not client.local:
+ up(self.gameIcon, data['large_image'])
+ self.setFontText(self.gameName, data['details'])
+ else:
+ self.gamePlate.hide()
+ self.underLyingButton.click()
+ except Exception as e:
+ print('Update error: %s' % e)
def error(self, error, traceback):
self.err = error
self.traceback = traceback
- print(self.error)
+ print(self.err)
self.underLyingButton2.click()
def errorMes(self):
@@ -202,6 +339,16 @@ def logout(self):
os.remove(privateFile)
sys.exit()
+ def reauthorize(self):
+ self.reauthing = True
+ self.keyInput.setText('')
+ self.updatePage(Page.LINK)
+ self.MainWindow.show()
+
+ def setStatus(self, message):
+ if self.page == Page.LOADING:
+ self.loadingText.setText(message)
+
class SystemTrayApp(QSystemTrayIcon):
def __init__(self, icon, parent):
QSystemTrayIcon.__init__(self, icon, parent)
@@ -224,6 +371,11 @@ def reopen(self):
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
+ lock = acquireLock()
+ if not lock:
+ QMessageBox.warning(None, '3DS-RPC', '3DS-RPC is already running.')
+ sys.exit(0)
+
MainWindow = QMainWindow()
window = GUI(MainWindow)
@@ -232,19 +384,20 @@ def reopen(self):
window.setupUi(MainWindow)
window.selfService(app)
- if not friendCode:
+ if not apiKey:
MainWindow.show()
app.exec_()
- client = Client(friendCode, config, GUI = window)
- client.connect()
- threading.Thread(target = client.background, daemon = True).start()
- while not client.userData and not window.err:
- pass
+ config['apiKey'] = apiKey
+
+ client = Client(apiKey, config, GUI = window)
window.state = True
window.setupUi(MainWindow)
window.selfService(app)
- window.updatePage(2)
+ window.updatePage(Page.LOADING)
MainWindow.show()
+ client.connect()
+ threading.Thread(target = client.background, daemon = True).start()
+
sys.exit(app.exec_())
diff --git a/client.py b/client.py
index 96c4986..e753f43 100644
--- a/client.py
+++ b/client.py
@@ -4,26 +4,41 @@
import xmltodict, json
import pickle
import asyncio, threading
+from urllib.parse import urlsplit
try:
from api import *
except:
sys.path.append('../')
from api import *
import pypresence
+from PyQt5.QtCore import QLockFile
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
-local = False
-version = 0.31
-
-host = 'https://3dsrpc.com' # Change the host as you'd wish
-if local:
- host = 'http://127.0.0.1:2277'
-
-## The below contains 3dsrpc.com-specific information
-## You will have to provide your own 'bot' FC if you are planning
-## on running your own front and backend.
-friend_code_to_principal_id(nintendoBotFC) # A quick verification check
+version = 0.32
+
+# Endpoint presets. The selection lives client-side in the config.
+officialHost = 'https://3dsrpc.com'
+host = officialHost
+
+def getHost(endpoint:str, customEndpoint:str = '') -> str:
+ endpoint = (endpoint or 'official').lower()
+ if endpoint == 'custom' and customEndpoint:
+ return customEndpoint.strip().rstrip('/')
+ return officialHost
+
+def validateEndpoint(url:str) -> str:
+ url = (url or '').strip()
+ if not url:
+ raise ValueError('Please enter a custom endpoint URL.')
+ parts = urlsplit(url)
+ if parts.scheme not in ('http', 'https') or not parts.netloc:
+ raise ValueError('Custom endpoint must be a full URL, e.g. https://your-server.example.com')
+ if parts.path not in ('', '/'):
+ raise ValueError('Custom endpoint cannot include a subpath. Use just the host, e.g. https://your-server.example.com')
+ if parts.query or parts.fragment:
+ raise ValueError('Custom endpoint cannot include a query string or fragment.')
+ return url.rstrip('/')
_REGION = typing.Literal['ALL', 'US', 'JP', 'GB', 'KR', 'TW']
path = getAppPath()
@@ -32,37 +47,68 @@
# Config template
configTemplate = {
- 'friendCode': '',
+ 'apiKey': '',
+ 'endpoint': 'official',
+ 'customEndpoint': '',
'showElapsed': True,
'showProfileButton': False,
'showSmallImage': False,
- 'fetchTime': 30,
+ 'fetchTime': 60,
}
+lockPath = os.path.join(path, '3dsrpc.lock')
+
+def acquireLock(timeout:int = 100):
+ lock = QLockFile(lockPath)
+ return lock if lock.tryLock(timeout) else None
+
def log(text:str):
with open(logFile, 'a') as file:
file.write('%s: %s\n' % (time.time(), text.replace('\n',' ')))
print(Color.RED + text)
+class RateLimitedError(Exception):
+ def __init__(self, retry_after = None):
+ try:
+ self.wait = float(retry_after)
+ except (TypeError, ValueError):
+ self.wait = 30
+ super().__init__('Rate limited. Waiting %ss.' % self.wait)
+
+class BackendOfflineError(Exception):
+ def __init__(self):
+ super().__init__('Backend currently offline. Retrying later.')
+
+class InvalidAPIKeyError(Exception):
+ def __init__(self):
+ super().__init__('Invalid console API key.')
+
class Client():
- def __init__(self, friendCode: str, config:dict, *, GUI = None):
- ### Maintain typing ###
- friendCode = str(principal_id_to_friend_code(friend_code_to_principal_id(friendCode))).zfill(12) # Friend Code check
- with open(privateFile, 'w') as file: # Save FC and config to file
+ def __init__(self, apiKey: str, config:dict, *, GUI = None):
+ with open(privateFile, 'w') as file: # Save API key and config to file
js = configTemplate
- js['friendCode'] = friendCode
+ js['apiKey'] = apiKey
for key in config.keys():
if key in configTemplate.keys():
js[key] = config[key]
file.write(json.dumps(js))
- # FC variables
- self.friendCode = friendCode
+ # Server-auth variables
+ self.apiKey = js.get('apiKey', '')
+
+ # Endpoint variables
+ self.endpoint = js.get('endpoint', 'official')
+ self.customEndpoint = js.get('customEndpoint', '')
+ self.host = getHost(self.endpoint, self.customEndpoint)
+ self.local = '127.0.0.1' in self.host or 'localhost' in self.host
# Client-config
self.connected = False
self.userData = {}
+ # Re-authentication coordination
+ self.authWait = threading.Event()
+
# Discord-related variables
self.currentGame = {'@id': None}
self.showElapsed = js['showElapsed']
@@ -84,13 +130,23 @@ def reflectConfig(self):
js[key] = self.__dict__[key]
file.write(json.dumps(js))
+ # Change the endpoint (official / custom)
+ def setEndpoint(self, endpoint:str, customEndpoint:str = ''):
+ self.endpoint = endpoint
+ self.customEndpoint = customEndpoint
+ self.host = getHost(endpoint, customEndpoint)
+ self.local = '127.0.0.1' in self.host or 'localhost' in self.host
+ self.reflectConfig()
+
+ # Set a new API key after an invalid/expired one
+ def updateKey(self, apiKey:str):
+ self.apiKey = apiKey
+ self.authWait.set()
+ self.reflectConfig()
+
# Get from API
def APIget(self, route:str, content:dict = {}):
- return requests.get(host + '/api/' + route, data = content, headers = {'User-Agent':'3DS-RPC/%s' % version,})
-
- # Post to API
- def APIpost(self, route:str, content:dict = {}):
- return requests.post(host + '/api/' + route, data = content, headers = {'User-Agent':'3DS-RPC/%s' % version,})
+ return requests.get(self.host + '/api/' + route, params = content, headers = {'User-Agent':'3DS-RPC/%s' % version, 'X-API-KEY': self.apiKey,})
# Connect to PyPresence
def connect(self, pipe:str = '0'):
@@ -99,7 +155,7 @@ def connect(self, pipe:str = '0'):
self.rpc.connect()
except Exception as e:
if self.GUI:
- self.GUI.error(str(e), traceback.format_exc())
+ self.GUI.bridge.errored.emit(str(e), traceback.format_exc())
else:
raise e
self.connected = True
@@ -113,39 +169,35 @@ def disconnect(self):
self.rpc = None
self.connected = False
- def login(self):
- r = self.APIpost('user/create/%s' % self.friendCode)
- try:
- r = r.json()
- except:
- APIExcept(r)
- if r['Exception']:
- if not 'UNIQUE constraint failed: friends.friendCode' in r['Exception']['Error']:
- raise APIException(r['Exception'])
- return r
-
def fetch(self):
- r = self.APIget('user/%s' % self.friendCode)
+ r = self.APIget('user/activeConsoles')
+ if r.status_code == 429:
+ raise RateLimitedError(r.headers.get('Retry-After'))
+ if r.status_code == 502:
+ raise BackendOfflineError()
try:
r = r.json()
except:
APIExcept(r)
if r['Exception']:
- if 'not recognized' in r['Exception']['Error']:
- print('%sRemember, the bot\'s friend code is as follows:\n%s%s' % (Color.YELLOW, '-'.join(nintendoBotFC[i:i+4] for i in range(0, len(nintendoBotFC), 4)), Color.DEFAULT))
+ error = r['Exception']['Error']
+ if 'offline' in error:
+ raise BackendOfflineError()
+ if 'invalid console API key' in error:
+ raise InvalidAPIKeyError()
raise APIException(r['Exception'])
return r
def loop(self):
userData = self.fetch();self.userData = userData
- presence = userData['User']['Presence']
+ console = userData['consoles'][0] if userData.get('consoles') else None
- _pass = None
- if userData['User']['online'] and presence:
+ logger = 'Update'
+ if console and console.get('online') and console.get('Presence'):
+ presence = console['Presence']
game = presence['game']
- logger = 'Update'
if self.currentGame != game:
logger += ' [%s -> %s]' % (self.currentGame['@id'], game['@id'])
self.currentGame = game
@@ -158,18 +210,18 @@ def loop(self):
# Include View Profile setting?
# Certainly something when presence['joinable'] == True
}
- if game['icon_url']:
- kwargs['large_image'] = game['icon_url'].replace('/cdn/', host + '/cdn/')
+ if game.get('icon_url'):
+ kwargs['large_image'] = game['icon_url'].replace('/cdn/', self.host + '/cdn/')
kwargs['large_text'] = game['name']
- if presence['gameDescription']:
+ if presence.get('gameDescription'):
kwargs['state'] = presence['gameDescription']
- if self.showProfileButton and userData['User']['username']:
- kwargs['buttons'] = [{'label': 'Profile', 'url': host + '/user/' + userData['User']['friendCode']},]
+ if self.showProfileButton and console.get('username'):
+ kwargs['buttons'] = [{'label': 'Profile', 'url': self.host + '/user/' + console['friendCode']},]
if self.showElapsed:
kwargs['start'] = self.start
- if self.showSmallImage and userData['User']['username'] and game['icon_url']:
- kwargs['small_image'] = userData['User']['mii']['face']
- kwargs['small_text'] = '-'.join(userData['User']['friendCode'][i:i+4] for i in range(0, 12, 4))
+ if self.showSmallImage and console.get('username') and game.get('icon_url'):
+ kwargs['small_image'] = console['mii']['face']
+ kwargs['small_text'] = '-'.join(console['friendCode'][i:i+4] for i in range(0, 12, 4))
for key in list(kwargs): # Blatant rip from OpenEmuRPC (also made by me. Check it out if you want)
if isinstance(kwargs[key], str) and not 'image' in key:
if len(kwargs[key]) < 2:
@@ -177,57 +229,95 @@ def loop(self):
elif len(kwargs[key]) > 128:
kwargs[key] = kwargs[key][:128]
if self.connected:self.rpc.update(**kwargs)
- if self.GUI:self.GUI.update(kwargs)
+ if self.GUI:self.GUI.bridge.updated.emit(kwargs)
else:
logger = 'Clear [%s -> %s]' % (self.currentGame['@id'], None)
self.currentGame = {'@id': None}
if self.connected:self.rpc.clear()
- if self.GUI:self.GUI.update(None)
+ if self.GUI:self.GUI.bridge.updated.emit(None)
self.gameLog.append(logger)
def background(self):
try:
- self.login() # Create account if not yet existent
while True:
- self.loop()
- time.sleep(self.fetchTime) # Wait 30 seconds between calls
+ try:
+ self.loop()
+ except InvalidAPIKeyError:
+ log('Invalid API key.')
+ if self.GUI:
+ self.GUI.bridge.reauthRequested.emit()
+ else:
+ log('Use the \'apikey\' command to set a new key.')
+ self.authWait.wait()
+ self.authWait.clear()
+ except (RateLimitedError, BackendOfflineError) as e:
+ log(str(e))
+ if self.GUI:
+ self.GUI.bridge.statusChanged.emit('Rate limited. Retrying shortly...' if isinstance(e, RateLimitedError) else 'Backend offline. Retrying...')
+ time.sleep(getattr(e, 'wait', self.fetchTime))
+ time.sleep(self.fetchTime) # Wait 60 seconds between calls
except Exception as e:
if self.GUI:
- self.GUI.error(str(e), traceback.format_exc())
+ self.GUI.bridge.errored.emit(str(e), traceback.format_exc())
else:
log('Failed\n' + str(e))
print(traceback.format_exc())
os._exit(0)
def main():
- friendCode = None
+ lock = acquireLock()
+ if not lock:
+ print('%sAnother instance of 3DS-RPC is already running.%s' % (Color.RED, Color.DEFAULT))
+ os._exit(0)
+
+ apiKey = None
+ config = {}
- # Create directory for logging and friend code saving
+ # Create directory for logging and API key saving
if not os.path.isdir(path):
os.mkdir(path)
try:
if os.path.isfile(privateFile):
with open(privateFile, 'r') as file:
js = json.loads(file.read())
- friendCode = js['friendCode']
+ apiKey = js.get('apiKey', '')
config = js
- del config['friendCode']
- else:
- raise Exception()
+ config.pop('apiKey', None)
except:
- print('%sPlease take this time to add the bot\'s FC to your target 3DS\' friends list.\n%sBot FC: %s%s' % (Color.YELLOW, Color.DEFAULT, Color.BLUE, '-'.join(nintendoBotFC[i:i+4] for i in range(0, 12, 4))))
- input('%s[Press enter to continue]%s' % (Color.GREEN, Color.DEFAULT))
- friendCode = input('Please enter your 3DS\' friend code\n> %s' % Color.PURPLE)
+ apiKey = None
config = {}
+
+ # The endpoint must be known before we can connect, so ask first if it's missing
+ if not config.get('endpoint'):
+ print('%sWhich endpoint would you like to use? (official or custom)%s' % (Color.YELLOW, Color.DEFAULT))
+ endpoint = input('Please enter your endpoint (official or custom)\n> %s' % Color.PURPLE)
+ if endpoint.lower() not in ('official', 'custom'):
+ endpoint = 'official'
+ config['endpoint'] = endpoint
+ if endpoint.lower() == 'custom':
+ while True:
+ customUrl = input('Please enter your custom endpoint URL\n> %s' % Color.PURPLE)
+ try:
+ config['customEndpoint'] = validateEndpoint(customUrl)
+ break
+ except ValueError as e:
+ print('%s%s%s' % (Color.RED, e, Color.DEFAULT))
+ print(Color.DEFAULT, end = '')
+
+ if not apiKey:
+ print('%sPlease grab your API key from the Settings page on your endpoint%s' % (Color.YELLOW, Color.DEFAULT))
+ apiKey = input('Please enter your API key\n> %s' % Color.PURPLE)
print(Color.DEFAULT, end = '')
try:
- client = Client(friendCode, config)
- except (AssertionError, FriendCodeValidityError) as e:
+ client = Client(apiKey, config)
+ except (AssertionError) as e:
if os.path.isfile(privateFile):
os.remove(privateFile)
raise e
+ print('%sConnecting to %s%s' % (Color.BLUE, client.host, Color.DEFAULT))
+
# Begin main thread for user configuration
con = Console(client)
try:
diff --git a/layout/__init__.py b/layout/__init__.py
index 9d45b24..2002c82 100644
--- a/layout/__init__.py
+++ b/layout/__init__.py
@@ -1,252 +1,356 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'mainwindow.ui'
-#
-# Created by: PyQt5 UI code generator 5.15.7
-#
-# WARNING: Any manual changes made to this file will be lost when pyuic5 is
-# run again. Do not edit this file unless you know what you are doing.
-
-
-from PyQt5 import QtCore, QtGui, QtWidgets
-
-
-class Ui_MainWindow(object):
- def setupUi(self, MainWindow):
- MainWindow.setObjectName("MainWindow")
- MainWindow.resize(600, 600)
- self.stackedWidget = QtWidgets.QStackedWidget(MainWindow)
- self.stackedWidget.setGeometry(QtCore.QRect(0, 0, 601, 601))
- self.stackedWidget.setObjectName("stackedWidget")
- self.bot = QtWidgets.QWidget()
- self.bot.setObjectName("bot")
- self.description = QtWidgets.QLabel(self.bot)
- self.description.setGeometry(QtCore.QRect(130, 80, 331, 71))
- self.description.setAlignment(QtCore.Qt.AlignCenter)
- self.description.setWordWrap(True)
- self.description.setObjectName("description")
- self.section = QtWidgets.QGroupBox(self.bot)
- self.section.setGeometry(QtCore.QRect(40, 190, 511, 211))
- self.section.setTitle("")
- self.section.setObjectName("section")
- self.continueButton = QtWidgets.QPushButton(self.section)
- self.continueButton.setGeometry(QtCore.QRect(160, 140, 181, 31))
- self.continueButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.continueButton.setObjectName("continueButton")
- self.botFCLabel = QtWidgets.QLabel(self.section)
- self.botFCLabel.setGeometry(QtCore.QRect(90, 40, 331, 71))
- font = QtGui.QFont()
- font.setPointSize(20)
- font.setBold(True)
- font.setUnderline(True)
- font.setWeight(75)
- self.botFCLabel.setFont(font)
- self.botFCLabel.setAlignment(QtCore.Qt.AlignCenter)
- self.botFCLabel.setWordWrap(True)
- self.botFCLabel.setObjectName("botFCLabel")
- self.title = QtWidgets.QLabel(self.bot)
- self.title.setGeometry(QtCore.QRect(100, 20, 391, 61))
- font = QtGui.QFont()
- font.setPointSize(31)
- self.title.setFont(font)
- self.title.setAlignment(QtCore.Qt.AlignCenter)
- self.title.setObjectName("title")
- self.description_2 = QtWidgets.QLabel(self.bot)
- self.description_2.setGeometry(QtCore.QRect(130, 420, 331, 81))
- self.description_2.setAlignment(QtCore.Qt.AlignCenter)
- self.description_2.setWordWrap(True)
- self.description_2.setOpenExternalLinks(True)
- self.description_2.setObjectName("description_2")
- self.stackedWidget.addWidget(self.bot)
- self.link = QtWidgets.QWidget()
- self.link.setObjectName("link")
- self.description2 = QtWidgets.QLabel(self.link)
- self.description2.setGeometry(QtCore.QRect(130, 80, 331, 71))
- self.description2.setAlignment(QtCore.Qt.AlignCenter)
- self.description2.setWordWrap(True)
- self.description2.setObjectName("description2")
- self.section2 = QtWidgets.QGroupBox(self.link)
- self.section2.setGeometry(QtCore.QRect(40, 190, 511, 211))
- self.section2.setTitle("")
- self.section2.setObjectName("section2")
- self.loginButton = QtWidgets.QPushButton(self.section2)
- self.loginButton.setGeometry(QtCore.QRect(160, 140, 181, 31))
- self.loginButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.loginButton.setObjectName("loginButton")
- self.fcInput = QtWidgets.QLineEdit(self.section2)
- self.fcInput.setGeometry(QtCore.QRect(80, 70, 341, 24))
- self.fcInput.setObjectName("fcInput")
- self.title2 = QtWidgets.QLabel(self.link)
- self.title2.setGeometry(QtCore.QRect(100, 20, 391, 61))
- font = QtGui.QFont()
- font.setPointSize(31)
- self.title2.setFont(font)
- self.title2.setAlignment(QtCore.Qt.AlignCenter)
- self.title2.setObjectName("title2")
- self.description_3 = QtWidgets.QLabel(self.link)
- self.description_3.setGeometry(QtCore.QRect(130, 420, 331, 81))
- self.description_3.setAlignment(QtCore.Qt.AlignCenter)
- self.description_3.setWordWrap(True)
- self.description_3.setOpenExternalLinks(True)
- self.description_3.setObjectName("description_3")
- self.stackedWidget.addWidget(self.link)
- self.main = QtWidgets.QWidget()
- self.main.setObjectName("main")
- self.settingsButton = QtWidgets.QPushButton(self.main)
- self.settingsButton.setGeometry(QtCore.QRect(0, -20, 601, 61))
- self.settingsButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.settingsButton.setObjectName("settingsButton")
- self.friendCard = QtWidgets.QGroupBox(self.main)
- self.friendCard.setGeometry(QtCore.QRect(80, 190, 451, 231))
- self.friendCard.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.friendCard.setTitle("")
- self.friendCard.setObjectName("friendCard")
- self.namePlate = QtWidgets.QGroupBox(self.friendCard)
- self.namePlate.setGeometry(QtCore.QRect(0, 170, 451, 81))
- self.namePlate.setTitle("")
- self.namePlate.setObjectName("namePlate")
- self.username = QtWidgets.QLabel(self.namePlate)
- self.username.setGeometry(QtCore.QRect(280, 10, 151, 41))
- self.username.setText("")
- self.username.setAlignment(QtCore.Qt.AlignCenter)
- self.username.setObjectName("username")
- self.status = QtWidgets.QLabel(self.friendCard)
- self.status.setGeometry(QtCore.QRect(10, 10, 101, 21))
- self.status.setText("")
- self.status.setObjectName("status")
- self.miiLabel = QtWidgets.QLabel(self.friendCard)
- self.miiLabel.setGeometry(QtCore.QRect(280, 20, 151, 151))
- self.miiLabel.setText("")
- self.miiLabel.setObjectName("miiLabel")
- self.closeButton = QtWidgets.QPushButton(self.main)
- self.closeButton.setGeometry(QtCore.QRect(70, 570, 461, 61))
- self.closeButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.closeButton.setObjectName("closeButton")
- self.gamePlate = QtWidgets.QGroupBox(self.main)
- self.gamePlate.setGeometry(QtCore.QRect(30, 230, 321, 111))
- self.gamePlate.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.gamePlate.setTitle("")
- self.gamePlate.setObjectName("gamePlate")
- self.gameIcon = QtWidgets.QLabel(self.gamePlate)
- self.gameIcon.setGeometry(QtCore.QRect(20, 30, 51, 51))
- self.gameIcon.setText("")
- self.gameIcon.setObjectName("gameIcon")
- self.gameName = QtWidgets.QLabel(self.gamePlate)
- self.gameName.setGeometry(QtCore.QRect(80, 40, 231, 31))
- self.gameName.setText("")
- self.gameName.setObjectName("gameName")
- self.stackedWidget.addWidget(self.main)
- self.settings = QtWidgets.QWidget()
- self.settings.setObjectName("settings")
- self.okButton = QtWidgets.QPushButton(self.settings)
- self.okButton.setGeometry(QtCore.QRect(-1, 563, 601, 41))
- self.okButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.okButton.setObjectName("okButton")
- self.showElapsedOff = QtWidgets.QPushButton(self.settings)
- self.showElapsedOff.setGeometry(QtCore.QRect(30, 70, 270, 61))
- self.showElapsedOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showElapsedOff.setObjectName("showElapsedOff")
- self.showElapsedOn = QtWidgets.QPushButton(self.settings)
- self.showElapsedOn.setGeometry(QtCore.QRect(300, 70, 270, 61))
- self.showElapsedOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showElapsedOn.setObjectName("showElapsedOn")
- self.showProfileButtonOff = QtWidgets.QPushButton(self.settings)
- self.showProfileButtonOff.setGeometry(QtCore.QRect(30, 220, 270, 61))
- self.showProfileButtonOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showProfileButtonOff.setObjectName("showProfileButtonOff")
- self.showProfileButtonOn = QtWidgets.QPushButton(self.settings)
- self.showProfileButtonOn.setGeometry(QtCore.QRect(300, 220, 270, 61))
- self.showProfileButtonOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showProfileButtonOn.setObjectName("showProfileButtonOn")
- self.showSmallImageOff = QtWidgets.QPushButton(self.settings)
- self.showSmallImageOff.setGeometry(QtCore.QRect(30, 370, 270, 61))
- self.showSmallImageOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showSmallImageOff.setObjectName("showSmallImageOff")
- self.showSmallImageOn = QtWidgets.QPushButton(self.settings)
- self.showSmallImageOn.setGeometry(QtCore.QRect(300, 370, 270, 61))
- self.showSmallImageOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.showSmallImageOn.setObjectName("showSmallImageOn")
- self.showElapsedText = QtWidgets.QLabel(self.settings)
- self.showElapsedText.setGeometry(QtCore.QRect(15, 20, 571, 31))
- self.showElapsedText.setAlignment(QtCore.Qt.AlignCenter)
- self.showElapsedText.setObjectName("showElapsedText")
- self.showProfileButtonText = QtWidgets.QLabel(self.settings)
- self.showProfileButtonText.setGeometry(QtCore.QRect(15, 170, 571, 31))
- self.showProfileButtonText.setAlignment(QtCore.Qt.AlignCenter)
- self.showProfileButtonText.setObjectName("showProfileButtonText")
- self.showSmallImageText = QtWidgets.QLabel(self.settings)
- self.showSmallImageText.setGeometry(QtCore.QRect(15, 320, 571, 31))
- self.showSmallImageText.setAlignment(QtCore.Qt.AlignCenter)
- self.showSmallImageText.setObjectName("showSmallImageText")
- self.showElapsed = QtWidgets.QGroupBox(self.settings)
- self.showElapsed.setGeometry(QtCore.QRect(25, 65, 550, 71))
- self.showElapsed.setTitle("")
- self.showElapsed.setObjectName("showElapsed")
- self.okButtonLogout = QtWidgets.QPushButton(self.settings)
- self.okButtonLogout.setGeometry(QtCore.QRect(165, 470, 270, 61))
- self.okButtonLogout.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
- self.okButtonLogout.setObjectName("okButtonLogout")
- self.showProfileButton = QtWidgets.QGroupBox(self.settings)
- self.showProfileButton.setGeometry(QtCore.QRect(25, 215, 550, 71))
- self.showProfileButton.setTitle("")
- self.showProfileButton.setObjectName("showProfileButton")
- self.showSmallImage = QtWidgets.QGroupBox(self.settings)
- self.showSmallImage.setGeometry(QtCore.QRect(25, 365, 550, 71))
- self.showSmallImage.setTitle("")
- self.showSmallImage.setObjectName("showSmallImage")
- self.showSmallImage.raise_()
- self.showProfileButton.raise_()
- self.showElapsed.raise_()
- self.okButton.raise_()
- self.showElapsedOff.raise_()
- self.showElapsedOn.raise_()
- self.showProfileButtonOff.raise_()
- self.showProfileButtonOn.raise_()
- self.showSmallImageOff.raise_()
- self.showSmallImageOn.raise_()
- self.showElapsedText.raise_()
- self.showProfileButtonText.raise_()
- self.showSmallImageText.raise_()
- self.okButtonLogout.raise_()
- self.stackedWidget.addWidget(self.settings)
-
- self.retranslateUi(MainWindow)
- self.stackedWidget.setCurrentIndex(1)
- QtCore.QMetaObject.connectSlotsByName(MainWindow)
-
- def retranslateUi(self, MainWindow):
- _translate = QtCore.QCoreApplication.translate
- MainWindow.setWindowTitle(_translate("MainWindow", "3DS Rich Presence"))
- self.description.setText(_translate("MainWindow", "Add the bot\'s FC to your friend list before continuing to the next page."))
- self.continueButton.setText(_translate("MainWindow", "Continue"))
- self.botFCLabel.setText(_translate("MainWindow", "XXXX-XXXX-XXXX"))
- self.title.setText(_translate("MainWindow", "Link a Friend Code"))
- self.description_2.setText(_translate("MainWindow", "
Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
"))
- self.description2.setText(_translate("MainWindow", "Make certain that you have added the bot account\'s FC to your friendlist before pressing \"Log In\"."))
- self.loginButton.setText(_translate("MainWindow", "Log In"))
- self.fcInput.setPlaceholderText(_translate("MainWindow", "Enter your friend code here!"))
- self.title2.setText(_translate("MainWindow", "Link a Friend Code"))
- self.description_3.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
"))
- self.settingsButton.setText(_translate("MainWindow", "Settings"))
- self.closeButton.setText(_translate("MainWindow", "X Close"))
- self.okButton.setText(_translate("MainWindow", "OK"))
- self.showElapsedOff.setText(_translate("MainWindow", "No"))
- self.showElapsedOn.setText(_translate("MainWindow", "Yes"))
- self.showProfileButtonOff.setText(_translate("MainWindow", "No"))
- self.showProfileButtonOn.setText(_translate("MainWindow", "Yes"))
- self.showSmallImageOff.setText(_translate("MainWindow", "No"))
- self.showSmallImageOn.setText(_translate("MainWindow", "Yes"))
- self.showElapsedText.setText(_translate("MainWindow", "Allow others to see your current time in-game?"))
- self.showProfileButtonText.setText(_translate("MainWindow", "Allow others to click your status to view your profile?"))
- self.showSmallImageText.setText(_translate("MainWindow", "Show a small image of your Mii to Discord?"))
- self.okButtonLogout.setText(_translate("MainWindow", "Logout"))
-
-
-if __name__ == "__main__":
- import sys
- app = QtWidgets.QApplication(sys.argv)
- MainWindow = QtWidgets.QWidget()
- ui = Ui_MainWindow()
- ui.setupUi(MainWindow)
- MainWindow.show()
- sys.exit(app.exec_())
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'mainwindow.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.11
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(600, 600)
+ self.stackedWidget = QtWidgets.QStackedWidget(MainWindow)
+ self.stackedWidget.setGeometry(QtCore.QRect(0, 0, 601, 601))
+ self.stackedWidget.setObjectName("stackedWidget")
+ self.bot = QtWidgets.QWidget()
+ self.bot.setObjectName("bot")
+ self.welcomeLogo = QtWidgets.QLabel(self.bot)
+ self.welcomeLogo.setGeometry(QtCore.QRect(190, 25, 220, 220))
+ self.welcomeLogo.setText("")
+ self.welcomeLogo.setScaledContents(True)
+ self.welcomeLogo.setObjectName("welcomeLogo")
+ self.title = QtWidgets.QLabel(self.bot)
+ self.title.setGeometry(QtCore.QRect(100, 255, 400, 60))
+ font = QtGui.QFont()
+ font.setPointSize(31)
+ self.title.setFont(font)
+ self.title.setAlignment(QtCore.Qt.AlignCenter)
+ self.title.setObjectName("title")
+ self.description = QtWidgets.QLabel(self.bot)
+ self.description.setGeometry(QtCore.QRect(100, 315, 400, 75))
+ self.description.setAlignment(QtCore.Qt.AlignCenter)
+ self.description.setWordWrap(True)
+ self.description.setObjectName("description")
+ self.continueButton = QtWidgets.QPushButton(self.bot)
+ self.continueButton.setGeometry(QtCore.QRect(180, 410, 240, 45))
+ self.continueButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.continueButton.setObjectName("continueButton")
+ self.description_2 = QtWidgets.QLabel(self.bot)
+ self.description_2.setGeometry(QtCore.QRect(110, 500, 380, 80))
+ self.description_2.setAlignment(QtCore.Qt.AlignCenter)
+ self.description_2.setWordWrap(True)
+ self.description_2.setOpenExternalLinks(True)
+ self.description_2.setObjectName("description_2")
+ self.stackedWidget.addWidget(self.bot)
+ self.endpoint = QtWidgets.QWidget()
+ self.endpoint.setObjectName("endpoint")
+ self.endpointTitle = QtWidgets.QLabel(self.endpoint)
+ self.endpointTitle.setGeometry(QtCore.QRect(100, 50, 400, 60))
+ font = QtGui.QFont()
+ font.setPointSize(31)
+ self.endpointTitle.setFont(font)
+ self.endpointTitle.setAlignment(QtCore.Qt.AlignCenter)
+ self.endpointTitle.setObjectName("endpointTitle")
+ self.endpointDescription = QtWidgets.QLabel(self.endpoint)
+ self.endpointDescription.setGeometry(QtCore.QRect(100, 115, 400, 70))
+ self.endpointDescription.setAlignment(QtCore.Qt.AlignCenter)
+ self.endpointDescription.setWordWrap(True)
+ self.endpointDescription.setObjectName("endpointDescription")
+ self.section3 = QtWidgets.QGroupBox(self.endpoint)
+ self.section3.setGeometry(QtCore.QRect(80, 210, 440, 220))
+ self.section3.setTitle("")
+ self.section3.setObjectName("section3")
+ self.endpointInput = QtWidgets.QComboBox(self.section3)
+ self.endpointInput.setGeometry(QtCore.QRect(130, 50, 270, 30))
+ self.endpointInput.setObjectName("endpointInput")
+ self.endpointInput.addItem("")
+ self.endpointInput.addItem("")
+ self.endpointLabel = QtWidgets.QLabel(self.section3)
+ self.endpointLabel.setGeometry(QtCore.QRect(30, 50, 90, 30))
+ self.endpointLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
+ self.endpointLabel.setObjectName("endpointLabel")
+ self.customEndpointInput = QtWidgets.QLineEdit(self.section3)
+ self.customEndpointInput.setGeometry(QtCore.QRect(130, 100, 270, 30))
+ self.customEndpointInput.setObjectName("customEndpointInput")
+ self.endpointHint = QtWidgets.QLabel(self.section3)
+ self.endpointHint.setGeometry(QtCore.QRect(130, 133, 270, 22))
+ self.endpointHint.setText("")
+ self.endpointHint.setAlignment(QtCore.Qt.AlignCenter)
+ self.endpointHint.setObjectName("endpointHint")
+ self.saveEndpointButton = QtWidgets.QPushButton(self.section3)
+ self.saveEndpointButton.setGeometry(QtCore.QRect(230, 160, 150, 40))
+ self.saveEndpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.saveEndpointButton.setObjectName("saveEndpointButton")
+ self.backEndpointButton = QtWidgets.QPushButton(self.section3)
+ self.backEndpointButton.setGeometry(QtCore.QRect(60, 160, 150, 40))
+ self.backEndpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.backEndpointButton.setObjectName("backEndpointButton")
+ self.stackedWidget.addWidget(self.endpoint)
+ self.link = QtWidgets.QWidget()
+ self.link.setObjectName("link")
+ self.title2 = QtWidgets.QLabel(self.link)
+ self.title2.setGeometry(QtCore.QRect(100, 70, 400, 60))
+ font = QtGui.QFont()
+ font.setPointSize(31)
+ self.title2.setFont(font)
+ self.title2.setAlignment(QtCore.Qt.AlignCenter)
+ self.title2.setObjectName("title2")
+ self.description2 = QtWidgets.QLabel(self.link)
+ self.description2.setGeometry(QtCore.QRect(100, 140, 400, 60))
+ self.description2.setAlignment(QtCore.Qt.AlignCenter)
+ self.description2.setWordWrap(True)
+ self.description2.setObjectName("description2")
+ self.section2 = QtWidgets.QGroupBox(self.link)
+ self.section2.setGeometry(QtCore.QRect(80, 220, 440, 210))
+ self.section2.setTitle("")
+ self.section2.setObjectName("section2")
+ self.keyLabel = QtWidgets.QLabel(self.section2)
+ self.keyLabel.setGeometry(QtCore.QRect(30, 85, 90, 30))
+ self.keyLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
+ self.keyLabel.setObjectName("keyLabel")
+ self.keyInput = QtWidgets.QLineEdit(self.section2)
+ self.keyInput.setGeometry(QtCore.QRect(130, 85, 270, 30))
+ self.keyInput.setObjectName("keyInput")
+ self.loginButton = QtWidgets.QPushButton(self.section2)
+ self.loginButton.setGeometry(QtCore.QRect(140, 150, 220, 40))
+ self.loginButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.loginButton.setObjectName("loginButton")
+ self.description_3 = QtWidgets.QLabel(self.link)
+ self.description_3.setGeometry(QtCore.QRect(110, 490, 380, 80))
+ self.description_3.setAlignment(QtCore.Qt.AlignCenter)
+ self.description_3.setWordWrap(True)
+ self.description_3.setOpenExternalLinks(True)
+ self.description_3.setObjectName("description_3")
+ self.stackedWidget.addWidget(self.link)
+ self.loading = QtWidgets.QWidget()
+ self.loading.setObjectName("loading")
+ self.loadingTitle = QtWidgets.QLabel(self.loading)
+ self.loadingTitle.setGeometry(QtCore.QRect(100, 250, 400, 60))
+ font = QtGui.QFont()
+ font.setPointSize(31)
+ self.loadingTitle.setFont(font)
+ self.loadingTitle.setAlignment(QtCore.Qt.AlignCenter)
+ self.loadingTitle.setObjectName("loadingTitle")
+ self.loadingText = QtWidgets.QLabel(self.loading)
+ self.loadingText.setGeometry(QtCore.QRect(100, 320, 400, 40))
+ self.loadingText.setAlignment(QtCore.Qt.AlignCenter)
+ self.loadingText.setObjectName("loadingText")
+ self.stackedWidget.addWidget(self.loading)
+ self.main = QtWidgets.QWidget()
+ self.main.setObjectName("main")
+ self.settingsButton = QtWidgets.QPushButton(self.main)
+ self.settingsButton.setGeometry(QtCore.QRect(0, -20, 601, 61))
+ self.settingsButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.settingsButton.setObjectName("settingsButton")
+ self.refreshButton = QtWidgets.QPushButton(self.main)
+ self.refreshButton.setGeometry(QtCore.QRect(215, 65, 170, 40))
+ self.refreshButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.refreshButton.setObjectName("refreshButton")
+ self.friendCard = QtWidgets.QGroupBox(self.main)
+ self.friendCard.setGeometry(QtCore.QRect(80, 115, 451, 231))
+ self.friendCard.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.friendCard.setTitle("")
+ self.friendCard.setObjectName("friendCard")
+ self.namePlate = QtWidgets.QGroupBox(self.friendCard)
+ self.namePlate.setGeometry(QtCore.QRect(0, 170, 451, 81))
+ self.namePlate.setTitle("")
+ self.namePlate.setObjectName("namePlate")
+ self.username = QtWidgets.QLabel(self.namePlate)
+ self.username.setGeometry(QtCore.QRect(280, 10, 151, 41))
+ self.username.setText("")
+ self.username.setAlignment(QtCore.Qt.AlignCenter)
+ self.username.setObjectName("username")
+ self.status = QtWidgets.QLabel(self.friendCard)
+ self.status.setGeometry(QtCore.QRect(10, 10, 101, 21))
+ self.status.setText("")
+ self.status.setObjectName("status")
+ self.miiLabel = QtWidgets.QLabel(self.friendCard)
+ self.miiLabel.setGeometry(QtCore.QRect(280, 20, 151, 151))
+ self.miiLabel.setText("")
+ self.miiLabel.setObjectName("miiLabel")
+ self.closeButton = QtWidgets.QPushButton(self.main)
+ self.closeButton.setGeometry(QtCore.QRect(70, 570, 461, 61))
+ self.closeButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.closeButton.setObjectName("closeButton")
+ self.gamePlate = QtWidgets.QGroupBox(self.main)
+ self.gamePlate.setGeometry(QtCore.QRect(30, 155, 321, 111))
+ self.gamePlate.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.gamePlate.setVisible(False)
+ self.gamePlate.setTitle("")
+ self.gamePlate.setObjectName("gamePlate")
+ self.gameIcon = QtWidgets.QLabel(self.gamePlate)
+ self.gameIcon.setGeometry(QtCore.QRect(20, 30, 51, 51))
+ self.gameIcon.setText("")
+ self.gameIcon.setObjectName("gameIcon")
+ self.gameName = QtWidgets.QLabel(self.gamePlate)
+ self.gameName.setGeometry(QtCore.QRect(80, 40, 231, 31))
+ self.gameName.setText("")
+ self.gameName.setObjectName("gameName")
+ self.detailsCard = QtWidgets.QGroupBox(self.main)
+ self.detailsCard.setGeometry(QtCore.QRect(80, 375, 451, 135))
+ self.detailsCard.setTitle("")
+ self.detailsCard.setObjectName("detailsCard")
+ self.networkDetail = QtWidgets.QLabel(self.detailsCard)
+ self.networkDetail.setGeometry(QtCore.QRect(20, 44, 411, 24))
+ self.networkDetail.setText("")
+ self.networkDetail.setAlignment(QtCore.Qt.AlignCenter)
+ self.networkDetail.setObjectName("networkDetail")
+ self.endpointDetail = QtWidgets.QLabel(self.detailsCard)
+ self.endpointDetail.setGeometry(QtCore.QRect(20, 18, 411, 24))
+ self.endpointDetail.setText("")
+ self.endpointDetail.setAlignment(QtCore.Qt.AlignCenter)
+ self.endpointDetail.setObjectName("endpointDetail")
+ self.lastActiveDetail = QtWidgets.QLabel(self.detailsCard)
+ self.lastActiveDetail.setGeometry(QtCore.QRect(20, 70, 411, 24))
+ self.lastActiveDetail.setText("")
+ self.lastActiveDetail.setAlignment(QtCore.Qt.AlignCenter)
+ self.lastActiveDetail.setObjectName("lastActiveDetail")
+ self.lastOnlineDetail = QtWidgets.QLabel(self.detailsCard)
+ self.lastOnlineDetail.setGeometry(QtCore.QRect(20, 96, 411, 24))
+ self.lastOnlineDetail.setText("")
+ self.lastOnlineDetail.setAlignment(QtCore.Qt.AlignCenter)
+ self.lastOnlineDetail.setObjectName("lastOnlineDetail")
+ self.versionLabel = QtWidgets.QLabel(self.main)
+ self.versionLabel.setGeometry(QtCore.QRect(100, 528, 400, 25))
+ self.versionLabel.setText("")
+ self.versionLabel.setAlignment(QtCore.Qt.AlignCenter)
+ self.versionLabel.setObjectName("versionLabel")
+ self.stackedWidget.addWidget(self.main)
+ self.settings = QtWidgets.QWidget()
+ self.settings.setObjectName("settings")
+ self.okButton = QtWidgets.QPushButton(self.settings)
+ self.okButton.setGeometry(QtCore.QRect(-1, 563, 601, 41))
+ self.okButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.okButton.setObjectName("okButton")
+ self.showElapsedOff = QtWidgets.QPushButton(self.settings)
+ self.showElapsedOff.setGeometry(QtCore.QRect(30, 70, 270, 61))
+ self.showElapsedOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showElapsedOff.setObjectName("showElapsedOff")
+ self.showElapsedOn = QtWidgets.QPushButton(self.settings)
+ self.showElapsedOn.setGeometry(QtCore.QRect(300, 70, 270, 61))
+ self.showElapsedOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showElapsedOn.setObjectName("showElapsedOn")
+ self.showProfileButtonOff = QtWidgets.QPushButton(self.settings)
+ self.showProfileButtonOff.setGeometry(QtCore.QRect(30, 220, 270, 61))
+ self.showProfileButtonOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showProfileButtonOff.setObjectName("showProfileButtonOff")
+ self.showProfileButtonOn = QtWidgets.QPushButton(self.settings)
+ self.showProfileButtonOn.setGeometry(QtCore.QRect(300, 220, 270, 61))
+ self.showProfileButtonOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showProfileButtonOn.setObjectName("showProfileButtonOn")
+ self.showSmallImageOff = QtWidgets.QPushButton(self.settings)
+ self.showSmallImageOff.setGeometry(QtCore.QRect(30, 370, 270, 61))
+ self.showSmallImageOff.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showSmallImageOff.setObjectName("showSmallImageOff")
+ self.showSmallImageOn = QtWidgets.QPushButton(self.settings)
+ self.showSmallImageOn.setGeometry(QtCore.QRect(300, 370, 270, 61))
+ self.showSmallImageOn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.showSmallImageOn.setObjectName("showSmallImageOn")
+ self.showElapsedText = QtWidgets.QLabel(self.settings)
+ self.showElapsedText.setGeometry(QtCore.QRect(15, 20, 571, 31))
+ self.showElapsedText.setAlignment(QtCore.Qt.AlignCenter)
+ self.showElapsedText.setObjectName("showElapsedText")
+ self.showProfileButtonText = QtWidgets.QLabel(self.settings)
+ self.showProfileButtonText.setGeometry(QtCore.QRect(15, 170, 571, 31))
+ self.showProfileButtonText.setAlignment(QtCore.Qt.AlignCenter)
+ self.showProfileButtonText.setObjectName("showProfileButtonText")
+ self.showSmallImageText = QtWidgets.QLabel(self.settings)
+ self.showSmallImageText.setGeometry(QtCore.QRect(15, 320, 571, 31))
+ self.showSmallImageText.setAlignment(QtCore.Qt.AlignCenter)
+ self.showSmallImageText.setObjectName("showSmallImageText")
+ self.showElapsed = QtWidgets.QGroupBox(self.settings)
+ self.showElapsed.setGeometry(QtCore.QRect(25, 65, 550, 71))
+ self.showElapsed.setTitle("")
+ self.showElapsed.setObjectName("showElapsed")
+ self.okButtonLogout = QtWidgets.QPushButton(self.settings)
+ self.okButtonLogout.setGeometry(QtCore.QRect(165, 470, 270, 61))
+ self.okButtonLogout.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.okButtonLogout.setObjectName("okButtonLogout")
+ self.showProfileButton = QtWidgets.QGroupBox(self.settings)
+ self.showProfileButton.setGeometry(QtCore.QRect(25, 215, 550, 71))
+ self.showProfileButton.setTitle("")
+ self.showProfileButton.setObjectName("showProfileButton")
+ self.showSmallImage = QtWidgets.QGroupBox(self.settings)
+ self.showSmallImage.setGeometry(QtCore.QRect(25, 365, 550, 71))
+ self.showSmallImage.setTitle("")
+ self.showSmallImage.setObjectName("showSmallImage")
+ self.endpointButton = QtWidgets.QPushButton(self.settings)
+ self.endpointButton.setGeometry(QtCore.QRect(165, 531, 270, 27))
+ self.endpointButton.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
+ self.endpointButton.setObjectName("endpointButton")
+ self.showSmallImage.raise_()
+ self.showProfileButton.raise_()
+ self.showElapsed.raise_()
+ self.okButton.raise_()
+ self.showElapsedOff.raise_()
+ self.showElapsedOn.raise_()
+ self.showProfileButtonOff.raise_()
+ self.showProfileButtonOn.raise_()
+ self.showSmallImageOff.raise_()
+ self.showSmallImageOn.raise_()
+ self.showElapsedText.raise_()
+ self.showProfileButtonText.raise_()
+ self.showSmallImageText.raise_()
+ self.okButtonLogout.raise_()
+ self.endpointButton.raise_()
+ self.stackedWidget.addWidget(self.settings)
+
+ self.retranslateUi(MainWindow)
+ self.stackedWidget.setCurrentIndex(0)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ _translate = QtCore.QCoreApplication.translate
+ MainWindow.setWindowTitle(_translate("MainWindow", "3DS Rich Presence"))
+ self.title.setText(_translate("MainWindow", "Welcome"))
+ self.description.setText(_translate("MainWindow", "Choose your endpoint and enter your API key to get started."))
+ self.continueButton.setText(_translate("MainWindow", "Continue"))
+ self.description_2.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
"))
+ self.endpointTitle.setText(_translate("MainWindow", "Endpoint"))
+ self.endpointDescription.setText(_translate("MainWindow", "Choose which server to connect to. Official is hosted; custom lets you point the app at any compatible server."))
+ self.endpointInput.setItemText(0, _translate("MainWindow", "official"))
+ self.endpointInput.setItemText(1, _translate("MainWindow", "custom"))
+ self.endpointLabel.setText(_translate("MainWindow", "Endpoint"))
+ self.customEndpointInput.setPlaceholderText(_translate("MainWindow", "https://your-server.example.com"))
+ self.saveEndpointButton.setText(_translate("MainWindow", "Save"))
+ self.backEndpointButton.setText(_translate("MainWindow", "Back"))
+ self.title2.setText(_translate("MainWindow", "Enter your API Key"))
+ self.description2.setText(_translate("MainWindow", "You can find your API key on the Settings page of your endpoint."))
+ self.keyLabel.setText(_translate("MainWindow", "API Key"))
+ self.keyInput.setPlaceholderText(_translate("MainWindow", "Enter your API key from your endpoint\'s Settings page!"))
+ self.loginButton.setText(_translate("MainWindow", "Log In"))
+ self.description_3.setText(_translate("MainWindow", "Please be aware that by using this product you are agreeing to the Terms and Services listed on the MCMi460/3DS-RPC Github page.
"))
+ self.loadingTitle.setText(_translate("MainWindow", "Fetching profile..."))
+ self.loadingText.setText(_translate("MainWindow", "Retrieving your latest activity from the server."))
+ self.settingsButton.setText(_translate("MainWindow", "Settings"))
+ self.refreshButton.setText(_translate("MainWindow", "Refresh"))
+ self.closeButton.setText(_translate("MainWindow", "X Close"))
+ self.versionLabel.setStyleSheet(_translate("MainWindow", "color: #9E9E9E; font-size: 11px;"))
+ self.okButton.setText(_translate("MainWindow", "OK"))
+ self.showElapsedOff.setText(_translate("MainWindow", "No"))
+ self.showElapsedOn.setText(_translate("MainWindow", "Yes"))
+ self.showProfileButtonOff.setText(_translate("MainWindow", "No"))
+ self.showProfileButtonOn.setText(_translate("MainWindow", "Yes"))
+ self.showSmallImageOff.setText(_translate("MainWindow", "No"))
+ self.showSmallImageOn.setText(_translate("MainWindow", "Yes"))
+ self.showElapsedText.setText(_translate("MainWindow", "Allow others to see your current time in-game?"))
+ self.showProfileButtonText.setText(_translate("MainWindow", "Allow others to click your status to view your profile?"))
+ self.showSmallImageText.setText(_translate("MainWindow", "Show a small image of your Mii to Discord?"))
+ self.okButtonLogout.setText(_translate("MainWindow", "Logout"))
+ self.endpointButton.setText(_translate("MainWindow", "Endpoint"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ MainWindow = QtWidgets.QWidget()
+ ui = Ui_MainWindow()
+ ui.setupUi(MainWindow)
+ MainWindow.show()
+ sys.exit(app.exec_())
diff --git a/layout/mainwindow.ui b/layout/mainwindow.ui
index 32476d3..25a0dfe 100644
--- a/layout/mainwindow.ui
+++ b/layout/mainwindow.ui
@@ -23,91 +23,32 @@
- 1
+ 0
-
+
- 130
- 80
- 331
- 71
+ 190
+ 25
+ 220
+ 220
- Add the bot's FC to your friend list before continuing to the next page.
-
-
- Qt::AlignCenter
+
-
+
true
-
-
-
- 40
- 190
- 511
- 211
-
-
-
-
-
-
-
-
- 160
- 140
- 181
- 31
-
-
-
- PointingHandCursor
-
-
- Continue
-
-
-
-
-
- 90
- 40
- 331
- 71
-
-
-
-
- 20
- 75
- true
- true
-
-
-
- XXXX-XXXX-XXXX
-
-
- Qt::AlignCenter
-
-
- true
-
-
-
100
- 20
- 391
- 61
+ 255
+ 400
+ 60
@@ -116,19 +57,54 @@
- Link a Friend Code
+ Welcome
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 100
+ 315
+ 400
+ 75
+
+
+
+ Choose your endpoint and enter your API key to get started.
Qt::AlignCenter
+
+ true
+
+
+
+
+
+ 180
+ 410
+ 240
+ 45
+
+
+
+ PointingHandCursor
+
+
+ Continue
+
- 130
- 420
- 331
- 81
+ 110
+ 500
+ 380
+ 80
@@ -145,18 +121,39 @@
-
-
+
+
- 130
- 80
- 331
- 71
+ 100
+ 50
+ 400
+ 60
+
+
+
+
+ 31
+
+
+
+ Endpoint
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 100
+ 115
+ 400
+ 70
- Make certain that you have added the bot account's FC to your friendlist before pressing "Log In".
+ Choose which server to connect to. Official is hosted; custom lets you point the app at any compatible server.
Qt::AlignCenter
@@ -165,55 +162,125 @@
true
-
+
- 40
- 190
- 511
- 211
+ 80
+ 210
+ 440
+ 220
-
+
- 160
- 140
- 181
- 31
+ 130
+ 50
+ 270
+ 30
+
+
+ -
+
+ official
+
+
+ -
+
+ custom
+
+
+
+
+
+
+ 30
+ 50
+ 90
+ 30
+
+
+
+ Endpoint
+
+
+ Qt::AlignRight|Qt::AlignVCenter
+
+
+
+
+
+ 130
+ 100
+ 270
+ 30
+
+
+
+ https://your-server.example.com
+
+
+
+
+
+ 130
+ 133
+ 270
+ 22
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 230
+ 160
+ 150
+ 40
PointingHandCursor
- Log In
+ Save
-
+
- 80
- 70
- 341
- 24
+ 60
+ 160
+ 150
+ 40
-
- Enter your friend code here!
+
+ PointingHandCursor
+
+
+ Back
+
+
100
- 20
- 391
- 61
+ 70
+ 400
+ 60
@@ -222,19 +289,96 @@
- Link a Friend Code
+ Enter your API Key
Qt::AlignCenter
+
+
+
+ 100
+ 140
+ 400
+ 60
+
+
+
+ You can find your API key on the Settings page of your endpoint.
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+ 80
+ 220
+ 440
+ 210
+
+
+
+
+
+
+
+
+ 30
+ 85
+ 90
+ 30
+
+
+
+ API Key
+
+
+ Qt::AlignRight|Qt::AlignVCenter
+
+
+
+
+
+ 130
+ 85
+ 270
+ 30
+
+
+
+ Enter your API key from your endpoint's Settings page!
+
+
+
+
+
+ 140
+ 150
+ 220
+ 40
+
+
+
+ PointingHandCursor
+
+
+ Log In
+
+
+
- 130
- 420
- 331
- 81
+ 110
+ 490
+ 380
+ 80
@@ -250,8 +394,47 @@
true
-
-
+
+
+
+
+
+ 100
+ 250
+ 400
+ 60
+
+
+
+
+ 31
+
+
+
+ Fetching profile...
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 100
+ 320
+ 400
+ 40
+
+
+
+ Retrieving your latest activity from the server.
+
+
+ Qt::AlignCenter
+
+
+
+
@@ -268,11 +451,27 @@
Settings
+
+
+
+ 215
+ 65
+ 170
+ 40
+
+
+
+ PointingHandCursor
+
+
+ Refresh
+
+
80
- 190
+ 115
451
231
@@ -355,21 +554,24 @@
X Close
-
-
-
- 30
- 230
- 321
- 111
-
-
-
- PointingHandCursor
-
-
-
-
+
+
+
+ 30
+ 155
+ 321
+ 111
+
+
+
+ PointingHandCursor
+
+
+ false
+
+
+
+
@@ -397,8 +599,104 @@
-
-
+
+
+
+ 80
+ 375
+ 451
+ 135
+
+
+
+
+
+
+
+
+ 20
+ 44
+ 411
+ 24
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 18
+ 411
+ 24
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 70
+ 411
+ 24
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 20
+ 96
+ 411
+ 24
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+ 100
+ 528
+ 400
+ 25
+
+
+
+
+
+
+ Qt::AlignCenter
+
+
+ color: #9E9E9E; font-size: 11px;
+
+
+
+
@@ -614,6 +912,22 @@
+
+
+
+ 165
+ 531
+ 270
+ 27
+
+
+
+ PointingHandCursor
+
+
+ Endpoint
+
+
showSmallImage
showProfileButton
showElapsed
@@ -628,9 +942,10 @@
showProfileButtonText
showSmallImageText
okButtonLogout
+ endpointButton
-
+
\ No newline at end of file
From f7abce0ceb6bff7737b06ebc13575280a27200dd Mon Sep 17 00:00:00 2001
From: Phoenix/HotaruBlaze
Date: Wed, 2 Sep 2026 16:23:10 +0100
Subject: [PATCH 2/2] Pin setuptools for macOS
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index 9119d2e..679ebe8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,7 @@ pycryptodomex
# Necessary only for creating application bundles on macOS.
py2app; sys_platform == 'darwin'
+setuptools<84; sys_platform == 'darwin'
# Elsewhere, or for CLI usage, we bundle everything
# within a single executable via pyinstaller.
pyinstaller