class OrderableListItem(QWidget, Subscribable): """Available subscription types""" DELETED = 1 UPDATED = 2 """Registered subscription callbacks""" _subscriptions: dict[int, list[callable]] """The widgets layout""" _layout: QLayout """The items id""" id = 0 def __init__(self): # Init QWidget super().__init__() # Init Subscribable Subscribable.__init__( self, (OrderableListItem.DELETED, OrderableListItem.UPDATED)) self._layout = QVBoxLayout() self.setLayout(self._layout) # Set and increase id self.id = OrderableListItem.id OrderableListItem.id += 1 def delete(self): """Delete this item""" # Perform deletion subscriptions self._trigger_subscriptions(OrderableListItem.DELETED, item=self) # Delete all children while self._layout.count(): child: QLayoutItem = self._layout.takeAt(0) child_widget: QWidget = child.widget() child_widget.deleteLater() # Delete the item itself self.deleteLater() @abstractmethod def get_order_string(self): """Get the string that this item can be ordered by :returns str: The string to order this item by""" return ""
class ResultsList(QScrollArea): def __init__(self, parent=None): # List View of results # Only Title Search and Playlist Search use this QScrollArea.__init__(self) self.setParent(parent) # Make QScrollArea transparent self.setStyleSheet( "QScrollArea { background-color: transparent } .QFrame { background-color: transparent }" ) self.setWidgetResizable(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) # Add touch screen control to QScrollArea QScroller.grabGesture(self, QScroller.LeftMouseButtonGesture) # Set layout settings of the QScrollArea self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) # A separate QWidget is needed to properly use QScrollArea frame = QFrame() frame.setLayout(self.layout) self.setWidget(frame) def clearResults(self): # Clear the results in the list while self.layout.count(): item = self.layout.takeAt(0) if item.widget() is not None: item.widget().deleteLater() def addResults(self, results): # Add the results to the list # results (list): list of python dict representing results details (playlist or song search) self.results = results self.clearResults() if results: for i in self.results: item = self.ResultsListItem(i, self) self.layout.addWidget(item) self.layout.addStretch(1) else: # If the results are empty, display no result label = QLabel("没有结果/No Result") label.setStyleSheet("color: white") label.setAlignment(Qt.AlignCenter) font = QFont() font.setPointSize(35) label.setFont(font) self.layout.addWidget(label) class ResultsListItem(QToolButton): def __init__(self, result, parent=None): # An item of ResultsList # result: (dict) represents details (playlist or song search) QToolButton.__init__(self) self.setParent(parent) self.result = result # Button formatting self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.setFixedHeight(70) self.setAutoRaise(True) # TODO: change with global themes self.setStyleSheet( "QToolButton:pressed { background-color: rgba(255, 255, 255, 0.1)} QToolButton { background-color: rgba(255, 255, 255, 0.05); border: 1px solid white}" ) # Set layout self.layout = QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) # Depending on result type, add action when the item is clicked if result["type"] == "songs": self.formatTitle() self.clicked.connect(self.clickedSong) elif result["type"] == "playlists": self.formatPlaylist() self.clicked.connect(self.clickedPlaylist) self.setLayout(self.layout) def formatTitle(self): # Format the title for the ResultsList # Add space to the beginning of the ResultsListItem labelQueue = self.formattedLabel(QLabel("", self)) labelQueue.setFixedWidth(70) labelQueue.setAlignment(Qt.AlignCenter) self.layout.addWidget(labelQueue) # Artist name labelArtist = self.formattedLabel( QLabel(self.result["artist_name"])) labelArtist.setFixedWidth(300) self.layout.addWidget(labelArtist) # Song title labelTitle = self.formattedLabel( QLabel(self.result["song_title"])) self.layout.addWidget(labelTitle) # Add buttons for favourites and playlists # Favourite button self.favouriteButton = QToolButton() # Toggle Favourite Icon depending on DB if self.result["favourited"] == 0: self.favouriteButton.isFavourited = False self.favouriteButton.setIcon(QIcon("icons/star.svg")) else: self.favouriteButton.isFavourited = True self.favouriteButton.setIcon( QIcon("icons/star-yellow.svg")) self.favouriteButton.setIconSize(QSize(30, 30)) self.favouriteButton.setFixedSize(70, 70) self.favouriteButton.clicked.connect(self.clickedFavourite) self.layout.addWidget(self.favouriteButton) # Playlist button playlistButton = QToolButton() playlistButton.setIcon(QIcon("icons/music-player-2.svg")) playlistButton.setIconSize(QSize(30, 30)) playlistButton.setFixedSize(70, 70) playlistButton.clicked.connect(self.addToPlaylist) self.layout.addWidget(playlistButton) def formatPlaylist(self): # Add space to beginning of LisItem labelQueue = self.formattedLabel(QLabel("", self)) labelQueue.setFixedWidth(70) labelQueue.setAlignment(Qt.AlignCenter) self.layout.addWidget(labelQueue) # Playlist name labelPlaylist = self.formattedLabel( QLabel(self.result["playlist_name"])) self.layout.addWidget(labelPlaylist) # Remove playlist button btnRemove = QToolButton() btnRemove.setText("X") btnRemove.setFixedSize(70, 70) btnRemove.setStyleSheet("color: white") btnRemove.clicked.connect(self.removePlaylist) self.layout.addWidget(btnRemove) def formattedLabel(self, label): # Format a label font = QFont() font.setPixelSize(25) # TODO: change with global themes label.setStyleSheet("color: white") label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) label.setFont(font) return label def clickedSong(self): # Called when a song is clicked on # Add to queue self.window().songQueue.addSong(self.result) if len(self.window().songQueue.getQueue()) < 2: if not self.window().mediaPlayer.currentSong: # If this is the first song to queue, signal media player to start it self.window().mediaPlayer.skipSong() # Update scrolling text marquee self.window().mediaPlayer.updateMarquee() def clickedPlaylist(self): # Called when a playlist is clicked on self.window().content.addWidget( WindowSearch( "搜索全部/Search", self, self.window().content.currentWidget().counter + 1, self.result)) self.window().content.setCurrentIndex( self.window().content.currentWidget().counter + 1) def clickedFavourite(self): # Toggle Icon and DB state of song being favourited # For songs only if self.favouriteButton.isFavourited: self.favouriteButton.isFavourited = False self.favouriteButton.setIcon(QIcon("icons/star.svg")) else: self.favouriteButton.isFavourited = True self.favouriteButton.setIcon( QIcon("icons/star-yellow.svg")) DB.setFavouriteSong(self.result["song_id"]) def addToPlaylist(self): # Opens a popup for adding songs to playlists popup = PlaylistPopup(self.result, self.window()) popup.resize(1366, 768) popup.SIGNALS.CLOSE.connect(lambda: popup.close()) popup.show() def removePlaylist(self): # Remove a playlist DB.removePlaylist(self.result["playlist_id"]) self.window().content.currentWidget().results.results.remove( self.result) self.window().content.currentWidget().results.addResults( self.window().content.currentWidget().results.results)
class MainWidget(TritonWidget): def __init__(self, base): TritonWidget.__init__(self, base) self.addOTP = None self.closeEvent = self.widgetDeleted self.setWindowTitle('TritonAuth') self.setBackgroundColor(self, Qt.white) self.menu = QMenuBar() self.addMenu = self.menu.addMenu('Add') self.authAction = QAction('Authenticator', self) self.authAction.triggered.connect(self.openAddOTP) self.steamAction = QAction('Steam', self) self.steamAction.triggered.connect(self.openAddSteam) self.addMenu.addAction(self.authAction) self.addMenu.addAction(self.steamAction) self.sortMenu = self.menu.addMenu('Sort') self.nameAction = QAction('Sort by name', self) self.nameAction.triggered.connect(self.sortByName) self.sortMenu.addAction(self.nameAction) self.exportMenu = self.menu.addMenu('Export') self.andOTPAction = QAction('Export to andOTP', self) self.andOTPAction.triggered.connect(self.exportToAndOTP) self.exportMenu.addAction(self.andOTPAction) self.widget = QWidget() self.widget.setContentsMargins(10, 10, 10, 10) self.scrollArea = QScrollArea() self.scrollArea.setFixedSize(400, 495) self.scrollArea.setWidgetResizable(True) self.scrollWidget = QWidget() self.scrollLayout = QVBoxLayout(self.scrollWidget) self.scrollLayout.setAlignment(Qt.AlignTop) self.createAccounts() self.scrollArea.setWidget(self.scrollWidget) self.widgetLayout = QVBoxLayout(self.widget) self.widgetLayout.addWidget(self.scrollArea) self.boxLayout = QVBoxLayout(self) self.boxLayout.setContentsMargins(0, 5, 0, 0) self.boxLayout.addWidget(self.menu) self.boxLayout.addWidget(self.widget) self.setFixedSize(self.sizeHint()) self.center() self.show() def keyPressEvent(self, event): if type(event) != QKeyEvent: return letter = event.text().strip().lower() for i in range(self.scrollLayout.count()): widget = self.scrollLayout.itemAt(i).widget() if widget is not None and widget.name[0].lower() == letter: self.scrollArea.verticalScrollBar().setValue( widget.geometry().top()) return def widgetDeleted(self, arg): self.closeAddOTP() def closeAddOTP(self): if self.addOTP: self.addOTP.close() self.addOTP = None def addAccount(self, account): entry = EntryWidget(self.base, account) self.scrollLayout.addWidget(entry) def deleteAccount(self, account): for i in range(self.scrollLayout.count()): widget = self.scrollLayout.itemAt(i).widget() if widget.account == account: widget.close() def clearAccounts(self): for i in range(self.scrollLayout.count()): self.scrollLayout.itemAt(i).widget().close() def createAccounts(self): for account in self.base.getAccounts(): self.addAccount(account) def openAddOTP(self): self.closeAddOTP() self.addOTP = AddOTPWidget(self.base) def openAddSteam(self): self.closeAddOTP() self.addOTP = AddSteamWidget(self.base) def sortByName(self): self.base.sortAccountsByName() self.clearAccounts() self.createAccounts() def exportToAndOTP(self): accounts = [] for account in self.base.getAccounts(): type = account['type'] if type == Globals.OTPAuth: accounts.append({ 'secret': account['key'], 'digits': 6, 'period': 30, 'label': account['name'], 'type': 'TOTP', 'algorithm': 'SHA1', 'thumbnail': 'Default', 'last_used': 0, 'tags': [] }) elif type == Globals.SteamAuth: accounts.append({ 'secret': base64.b32encode(base64.b64decode( account['sharedSecret'])).decode('utf-8'), 'digits': 5, 'period': 30, 'label': account['name'], 'type': 'STEAM', 'algorithm': 'SHA1', 'thumbnail': 'Default', 'last_used': 0, 'tags': [] }) accounts = json.dumps(accounts) filename, _ = QFileDialog.getSaveFileName( self, 'Export to andOTP JSON file', '', 'All Files (*)') if filename: with open(filename, 'w') as file: file.write(accounts)
class OrderableListWidget(QScrollArea): """All available items in this list""" _item_list: list[OrderableListItem] """This lists actual widget""" _widget: QWidget """The widgets layout""" _layout: QLayout """Decides which way this list is ordered; 1 for ascending, -1 for descending""" _order_factor: int def __init__(self, order_asc=True, orientation_horizontal=False): """Init gui :type order_asc: bool :param order_asc: Whether to order the list ascending :type orientation_horizontal: bool :param orientation_horizontal: Should the list orientation be horizontal? """ super().__init__() if order_asc: self._order_factor = 1 else: self._order_factor = -1 self._widget = QWidget() self.setWidget(self._widget) self.setWidgetResizable(True) # Set layout if orientation_horizontal: self._layout = QHBoxLayout() else: self._layout = QVBoxLayout() self._widget.setLayout(self._layout) self._layout.setAlignment(Qt.AlignTop) self._item_list = [] def _get_order(self, list_item_a, list_item_b): """Defines this lists widget order :type list_item_a: OrderableListItem :param list_item_a: The first item to compare :type list_item_b: OrderableListItem :param list_item_b: The second item to compare :returns -1|0|1: list_item_a is: before, same, after list_item_b""" str_a: str = list_item_a.get_order_string() str_b: str = list_item_b.get_order_string() if str_a == str_b: return 0 elif str_a < str_b: return -1 * self._order_factor else: return 1 * self._order_factor def add(self, list_item): """Add a new item to the list :type list_item: OrderableListItem :param list_item: The item to add """ # Subscribe to changes list_item.subscribe(OrderableListItem.DELETED, self._item_deleted) list_item.subscribe(OrderableListItem.UPDATED, self._item_updated) # Make sure to add the item only once if list_item not in self._item_list: list_item_inserted = False self._item_list.append(list_item) # Walk all existing items for i in range(self._layout.count()): existing_item: OrderableListItem = self._layout.itemAt(i).widget() if 1 == self._get_order(existing_item, list_item): self._layout.insertWidget(i, list_item) list_item_inserted = True break if not list_item_inserted: self._layout.addWidget(list_item) def _item_deleted(self, item): """Delete an item from the list :type item: OrderableListItem :param item: The item to delete """ # See if the item exists in this list try: i: int = self._item_list.index(item) except ValueError: return # Delete the item self._item_list.pop(i) def _item_updated(self, item): """Update the list with the items new information :type item: OrderableListItem :param item: The item that was updated """ pass
class QueueList(QScrollArea): def __init__(self, parent=None): QScrollArea.__init__(self) self.setParent(parent) # Make QScrollArea transparent self.setStyleSheet( "QScrollArea { background-color: transparent } .QFrame { background-color: transparent }" ) self.setWidgetResizable(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) # Add Touch Gestures to menu. QScroller.grabGesture(self, QScroller.LeftMouseButtonGesture) # Set layout settings self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) # A singular widget frame is needed to set the scroll area properly. frame = QFrame() frame.setLayout(self.layout) self.setWidget(frame) self.updateQueue() class QueueListItem(QToolButton): def __init__(self, song, parent=None): QToolButton.__init__(self) self.setParent(parent) self.song = song # Button formatting self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.setFixedHeight(70) self.setAutoRaise(True) # TODO: change with global themes self.setStyleSheet( "QToolButton:pressed { background-color: rgba(255, 255, 255, 0.1)} QToolButton { background-color: rgba(255, 255, 255, 0.05); border: 1px solid white}" ) # Set layout self.layout = QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.createItem() self.setLayout(self.layout) def createItem(self): # Create a song item # Add Space to the beginning of the item labelQueue = self.formattedLabel(QLabel("", self)) labelQueue.setFixedWidth(70) labelQueue.setAlignment(Qt.AlignCenter) self.layout.addWidget(labelQueue) # Artist name labelArtist = self.formattedLabel( QLabel(self.song["artist_name"])) labelArtist.setFixedWidth(300) self.layout.addWidget(labelArtist) # Song Title labelTitle = self.formattedLabel( QLabel(self.song["song_title"])) self.layout.addWidget(labelTitle) # Set font (icon) size font = QFont() font.setPointSize(48) # Add buttons for queue-specific actions # Move the song up the queue btnMoveUp = QToolButton() btnMoveUp.setText("▲") btnMoveUp.setFixedSize(70, 70) btnMoveUp.setStyleSheet("color: white") btnMoveUp.setFont(font) btnMoveUp.clicked.connect(self.moveUp) self.layout.addWidget(btnMoveUp) # Move the song down the queue btnMoveDown = QToolButton() btnMoveDown.setText("▼") btnMoveDown.setFixedSize(70, 70) btnMoveDown.setStyleSheet("color: white") btnMoveDown.setFont(font) btnMoveDown.clicked.connect(self.moveDown) self.layout.addWidget(btnMoveDown) # Move the song to the top of the queue btnMoveTop = QToolButton() btnMoveTop.setText("⍏") btnMoveTop.setFixedSize(70, 70) btnMoveTop.setStyleSheet("color: white") btnMoveTop.setFont(font) btnMoveTop.clicked.connect(self.moveTop) self.layout.addWidget(btnMoveTop) # Play the song immediately btnPlay = QToolButton() btnPlay.setText("➤") btnPlay.setFixedSize(70, 70) btnPlay.setStyleSheet("color: white") btnPlay.setFont(font) btnPlay.clicked.connect(self.playSong) self.layout.addWidget(btnPlay) # Remove the song from the queue btnRemove = QToolButton() btnRemove.setText("X") btnRemove.setFixedSize(70, 70) btnRemove.setStyleSheet("color: white") btnRemove.setFont(font) btnRemove.clicked.connect(self.removeSong) self.layout.addWidget(btnRemove) def formattedLabel(self, label): font = QFont() font.setPixelSize(25) # TODO: change with global themes label.setStyleSheet("color: white") label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) label.setFont(font) return label def moveUp(self): # Move the song up the queue self.window().songQueue.moveUp(self.song) self.window().content.queueList.updateQueue() def moveDown(self): # Move the song down the queue self.window().songQueue.moveDown(self.song) self.window().content.queueList.updateQueue() def moveTop(self): # Move the song to the top of the queue self.window().songQueue.moveTop(self.song) self.window().content.queueList.updateQueue() def playSong(self): # Play the song immediately self.window().songQueue.moveTop(self.song) self.window().overlayBottom.buttonAction("next") self.window().content.queueList.updateQueue() def removeSong(self): # Remove the song from the queue self.window().songQueue.removeSong(self.song) self.window().content.queueList.updateQueue() if len(self.window().mediaPlayer.songQueue.getQueue()) == 0: self.window().mediaPlayer.updateMarquee() def updateQueue(self): # "Refresh" the queue visually while self.layout.count(): item = self.layout.takeAt(0) if item.widget() is not None: item.widget().deleteLater() for i in self.window().songQueue.getQueue(): item = self.QueueListItem(i, self) self.layout.addWidget(item) if self.window().songQueue.getQueue(): self.layout.addStretch(1) else: label = QLabel("没有结果/No Result") label.setStyleSheet("color: white") label.setAlignment(Qt.AlignCenter) font = QFont() font.setPointSize(35) label.setFont(font) self.layout.addWidget(label)