diff --git a/app/components/mode_filter_widget.py b/app/components/mode_filter_widget.py new file mode 100644 index 00000000..54093980 --- /dev/null +++ b/app/components/mode_filter_widget.py @@ -0,0 +1,92 @@ +import sys + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QApplication +from qfluentwidgets import CheckBox +from typing import List, Tuple + + +# TODO GameInfoInterface添加筛选功能 + +class ModeFilterWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.selected: List[int] = [] + + self.hBoxLayout = QHBoxLayout(self) + self.hBoxLayout.setAlignment(Qt.AlignCenter) + + # TODO 本地化 + self.rankSoloCheckBox = CheckBox(self.tr("Rank solo")) + self.rankFlexCheckBox = CheckBox(self.tr("Rank Flex")) + self.normalCheckBox = CheckBox(self.tr("Normal")) + self.aramCheckBox = CheckBox(self.tr("Aram")) + + self.checkBoxDict = { + self.rankSoloCheckBox: 420, # 单双排 + self.rankFlexCheckBox: 440, # 灵活组排 + self.normalCheckBox: 430, # 匹配模式 + self.aramCheckBox: 450 # 大乱斗 + } + + for checkBox, num in self.checkBoxDict.items(): + checkBox.stateChanged.connect(lambda state, num=num: self.updateSelected(state, num)) + + self.hBoxLayout.addWidget(self.rankSoloCheckBox, alignment=Qt.AlignCenter) + self.hBoxLayout.addWidget(self.rankFlexCheckBox, alignment=Qt.AlignCenter) + self.hBoxLayout.addWidget(self.normalCheckBox, alignment=Qt.AlignCenter) + self.hBoxLayout.addWidget(self.aramCheckBox, alignment=Qt.AlignCenter) + + self.setLayout(self.hBoxLayout) + + def updateSelected(self, state, num, callback=None): + if state == Qt.Checked: + if num not in self.selected: + self.selected.append(num) + else: + if num in self.selected: + self.selected.remove(num) + + if callback: + callback() + + def setCallback(self, func): + """ + @param func: check box状态改变时回调该方法 + @return: + """ + for checkBox, num in self.checkBoxDict.items(): + checkBox.stateChanged.connect(lambda state, num=num, func=func: self.updateSelected(state, num, func)) + + def getFilterMode(self) -> Tuple[int]: + """ + 获取选中的模式 + @return: + @rtype: Tuple[int] + """ + return set(self.selected) + + def setCheckBoxState(self, data: tuple): + """ + 设置复选框状态 + @param data: + @return: + """ + for checkBox, num in self.checkBoxDict.items(): + if num in data: + checkBox.setChecked(True) + else: + checkBox.setChecked(False) + + +if __name__ == '__main__': + QApplication.setHighDpiScaleFactorRoundingPolicy( + Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + + app = QApplication(sys.argv) + w = ModeFilterWidget() + w.show() + app.exec_() diff --git a/app/components/profile_level_icon_widget.py b/app/components/profile_level_icon_widget.py index 4fd11411..b35b0e99 100644 --- a/app/components/profile_level_icon_widget.py +++ b/app/components/profile_level_icon_widget.py @@ -102,8 +102,6 @@ def updateIcon(self, icon: str, xpSinceLastLevel=None, xpUntilNextLevel=None, te if text: self.progressRing.text = text - self.repaint() - if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/app/lol/tools.py b/app/lol/tools.py index ecdc0d31..e40aaedc 100644 --- a/app/lol/tools.py +++ b/app/lol/tools.py @@ -30,6 +30,9 @@ def translateTier(orig: str, short=False) -> str: def timeStampToStr(stamp): + """ + @param stamp: Millisecond timestamp + """ timeArray = time.localtime(stamp / 1000) return time.strftime("%Y/%m/%d %H:%M", timeArray) @@ -44,6 +47,7 @@ def secsToStr(secs): def processGameData(game): + timeStamp = game["gameCreation"] # 毫秒级时间戳 time = timeStampToStr(game['gameCreation']) shortTime = timeStampToShortStr(game['gameCreation']) gameId = game['gameId'] @@ -111,6 +115,7 @@ def processGameData(game): 'runeIcon': runeIcon, 'cs': cs, 'gold': gold, + 'timeStamp': timeStamp, } diff --git a/app/view/game_info_interface.py b/app/view/game_info_interface.py index 4d1d0b53..16b516d9 100644 --- a/app/view/game_info_interface.py +++ b/app/view/game_info_interface.py @@ -209,7 +209,7 @@ def updateSummoners(self, summoners): for summoner in summoners: summonerView = SummonerInfoView(summoner) - self.items[summoner["summonerId"]] = summonerView # 用 summonerId 避免空字符串 + self.items[str(summoner["summonerId"])] = summonerView # 用 summonerId 避免空字符串 self.vBoxLayout.addWidget(summonerView, stretch=1) if len(summoners) < 5: @@ -222,6 +222,7 @@ def clear(self): if item.widget(): item.widget().deleteLater() + self.items = {} class SummonerInfoView(QFrame): diff --git a/app/view/search_interface.py b/app/view/search_interface.py index 1214ea3e..54e6ed72 100644 --- a/app/view/search_interface.py +++ b/app/view/search_interface.py @@ -1,4 +1,5 @@ import threading +import time import pyperclip from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QFrame, @@ -6,12 +7,14 @@ from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QPixmap from qfluentwidgets import (SmoothScrollArea, LineEdit, PushButton, ToolButton, InfoBar, - InfoBarPosition, ToolTipFilter, ToolTipPosition, Theme, isDarkTheme, FlyoutViewBase, Flyout, CardWidget, IndeterminateProgressRing) + InfoBarPosition, ToolTipFilter, ToolTipPosition, Theme, isDarkTheme, FlyoutViewBase, Flyout, + CardWidget, IndeterminateProgressRing, FlyoutView, FlyoutAnimationType) from ..common.style_sheet import StyleSheet from ..common.icons import Icon from ..common.config import cfg from ..components.champion_icon_widget import RoundIcon +from ..components.mode_filter_widget import ModeFilterWidget from ..components.summoner_name_button import SummonerName from ..lol.connector import LolClientConnector, connector from ..lol.tools import processGameData, processGameDetailData @@ -41,6 +44,8 @@ def __init__(self, parnet=None): self.puuid = None self.games = [] + self.begIndex = 0 + self.triggerByButton = True self.__initWidget() @@ -103,6 +108,9 @@ def __onPrevButtonClicked(self): self.prevButton.setEnabled(False) def __onNextButtonClicked(self): + if self.currentIndex == 0: + self.begIndex = 0 + self.currentIndex += 1 if len(self.stackWidget) <= self.currentIndex: @@ -178,35 +186,52 @@ def updateTabs(self, begin, n): mainWindow.checkAndSwitchTo(mainWindow.searchInterface) def updateGames(self, page): - def _(): - if self.maxPage != None: - self.gamesInfoReady.emit(page) - return + def _(page, callback=None): + # retry = 0 + tmp_games_cnt = len(self.games) + endIndex = self.begIndex + 9 + while True: + games = connector.getSummonerGamesByPuuid( + self.puuid, self.begIndex, endIndex) + + for game in games["games"]: + if time.time() - game['gameCreation'] / 1000 > 60 * 60 * 24 * 365: + self.maxPage = page + break - count = 10 * (page + 1) - len(self.games) + if self.games: + # 避免重复添加 + if game['gameCreation'] >= self.games[-1]["timeStamp"]: + continue - begin = len(self.games) - end = begin + count - 1 + if game["queueId"] in self.window().searchInterface.filterData: + self.games += [processGameData(game)] - games = connector.getSummonerGamesByPuuid( - self.puuid, begin, end) + if len(self.games) - tmp_games_cnt >= 10: + self.maxPage = page + 1 + self.games = self.games[:10 * self.maxPage] + break - self.games += [processGameData(game) - for game in games["games"]] + # if retry >= 5: + # self.maxPage = page + # break - if page == 1: - if len(games["games"]) <= 10: - self.maxPage = 1 - else: - if len(games["games"]) < 10: - if len(games["games"]) == 0: - self.maxPage = page - else: - self.maxPage = page + 1 + self.begIndex = endIndex + 1 + endIndex += 10 + # retry += 1 + if callback: + callback() + # self.gamesInfoReady.emit(page) + + if page == 1: # 第一页时加载自身数据, 完成后切换; 并且并发加载下一页数据 + threading.Thread(target=_, args=(page, lambda page: self.gamesInfoReady.emit(page))).start() + else: # 除第一页外, 直接切换到该页, 并加载下一页; self.gamesInfoReady.emit(page) - threading.Thread(target=_).start() + self.nextButton.setEnabled(False) + threading.Thread(target=_, args=(page + 1, lambda: self.nextButton.setEnabled(True))).start() + def __onGamesInfoReady(self, page): if len(self.games) == 0: @@ -304,7 +329,6 @@ def __initLayout(self): def updateGame(self, game: dict): isCherry = game["queueId"] == 1700 - mapIcon = QPixmap(game["mapIcon"]).scaled( 54, 54, Qt.KeepAspectRatio, Qt.SmoothTransformation) if game["remake"]: @@ -926,12 +950,17 @@ class SearchInterface(SmoothScrollArea): def __init__(self, parent=None): super().__init__(parent) + self.filterData = (420, 440, 430, 450) # 默认全选 + self.filterTimer = threading.Timer(.5, self.__onSearchButtonClicked) + self.vBoxLayout = QVBoxLayout(self) self.searchLayout = QHBoxLayout() self.searchLineEdit = LineEdit() self.searchButton = PushButton(self.tr("Search 🔍")) self.careerButton = PushButton(self.tr("Career")) + self.filterButton = PushButton(self.tr("Filter")) + self.filterButton.clicked.connect(self.showFilterFlyout) self.gamesView = GamesView() self.currentSummonerName = None @@ -956,6 +985,7 @@ def __initLayout(self): self.searchLayout.addSpacing(5) self.searchLayout.addWidget(self.searchButton) self.searchLayout.addWidget(self.careerButton) + self.searchLayout.addWidget(self.filterButton) self.vBoxLayout.addLayout(self.searchLayout) self.vBoxLayout.addSpacing(5) @@ -1014,3 +1044,33 @@ def setEnabled(self, a0: bool) -> None: return super().setEnabled(a0) # def clear(self): + + def showFilterFlyout(self): + filterFlyout = FlyoutView("", "") + + filterBoxGroup = ModeFilterWidget() + filterBoxGroup.setCheckBoxState(self.filterData) + + def _(): + self.filterTimer.cancel() + + # 将选中状态同步到 interface + self.filterData = filterBoxGroup.getFilterMode() + self.gamesView.gamesTab.currentIndex = 0 + + # 消除频繁切换筛选条件带来的抖动 + self.filterTimer = threading.Timer(.5, self.__onSearchButtonClicked) + self.filterTimer.start() + # self.__onSearchButtonClicked() + filterBoxGroup.setCallback(_) + + filterFlyout.widgetLayout.addWidget(filterBoxGroup, 0, Qt.AlignCenter) + + filterFlyout.widgetLayout.setContentsMargins(1, 1, 1, 1) + filterFlyout.widgetLayout.setAlignment(Qt.AlignCenter) + + filterFlyout.viewLayout.setSpacing(0) + filterFlyout.viewLayout.setContentsMargins(1, 1, 1, 1) + + w = Flyout.make(filterFlyout, self.filterButton, self.window(), FlyoutAnimationType.DROP_DOWN) + filterFlyout.closed.connect(w.close)