diff --git a/data/TRND.ui b/data/main_window.ui similarity index 83% rename from data/TRND.ui rename to data/main_window.ui index b21ed27..b12ec3f 100644 --- a/data/TRND.ui +++ b/data/main_window.ui @@ -22,7 +22,7 @@ - + @@ -54,7 +54,7 @@ - + 0 @@ -296,6 +296,123 @@ + + + Оружие и моды + + + + + + Qt::Horizontal + + + 15 + + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + + 0 + 25 + + + + + 11 + 75 + true + + + + ОРУЖИЕ: + + + + + + + + 12 + + + + + + + + + + false + + + + + + + + + 0 + + + + + 0 + + + + + + 0 + 25 + + + + + 11 + 75 + true + + + + МОД: + + + + + + + + 12 + + + + + + + + + + false + + + + + + + + + Данные @@ -346,7 +463,7 @@ - + @@ -365,13 +482,13 @@ - 8 + 11 75 true - ВСЕГО: + Всего: @@ -390,7 +507,7 @@ - 8 + 11 75 true @@ -405,7 +522,7 @@ 1 - СЕЙЧАС: + Сейчас: Qt::AutoText @@ -425,7 +542,7 @@ - 10 + 12 75 true @@ -689,19 +806,19 @@ - 165 + 0 0 - 10 + 12 75 true - ВЕРСИЯ ПРИЛОЖЕНИЯ: + Версия: @@ -709,11 +826,14 @@ - 10 + 12 75 true + + + TextLabel diff --git a/libs/window.py b/libs/main_window.py similarity index 85% rename from libs/window.py rename to libs/main_window.py index af2ddd4..4e3e7a2 100644 --- a/libs/window.py +++ b/libs/main_window.py @@ -1,3 +1,5 @@ +import datetime +import logging import os import random import re @@ -9,19 +11,20 @@ from PyQt5 import uic from PyQt5.Qt import QDesktopServices, QUrl, QMenu, QApplication from PyQt5.QtCore import Qt, QTimer, QTimeLine, pyqtSlot, QPoint -from PyQt5.QtGui import QColor, QFont -from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QProgressDialog, QPushButton, QLabel, QProgressBar +from PyQt5.QtGui import QColor, QFont, QStandardItemModel +from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QProgressDialog, QPushButton, QLabel, QProgressBar, \ + QTreeView from PyQt5 import QtWinExtras from libs import utils from libs.qt_extends import JsonModel, ThreadController, showDetailedError -from libs.wiki_parser import get_data_from_wiki +from libs.wiki_parser import get_data_from_wiki, check_data_pages_update -class MyWindow(QMainWindow): +class MainWindow(QMainWindow): def __init__(self): - super(MyWindow, self).__init__() - uic.loadUi('data/TRND.ui', self) + super(MainWindow, self).__init__() + uic.loadUi('data/main_window.ui', self) self.date_format_str = '%d.%m.%Y %X' self.updateCheckTimeout = 60 @@ -32,9 +35,12 @@ def __init__(self): self.data_thread = None self.jsonData = None self.isUpdateQuestion = True + self.isDataWeaponsUpdateQuestion = True + self.isDataModsUpdateQuestion = True self.twMainModel = JsonModel() self.twRandomModel = JsonModel() + self.twPartsModsModel = JsonModel() self.updateTimer = QTimer() self.updateTimeoutTimer = QTimeLine() @@ -43,7 +49,21 @@ def __init__(self): self.btnClearData.clicked.connect(self.clear_json) self.btnRandomWeapon.clicked.connect(self.random_weapon) self.btnUpdateApp.clicked.connect(self.update_app) - self.leFind.textChanged[str].connect(self.find_and_display_weapons) + self.leFind.textChanged.connect(lambda value: self.find_and_display_rows( + value, + self.twMain, + self.twMainModel + )) + self.lePartsWeaponsFind.textChanged.connect(lambda value: self.find_and_display_rows( + value, + self.twPartsWeapons, + self.twMainModel + )) + self.lePartsModsFind.textChanged.connect(lambda value: self.find_and_display_rows( + value, + self.twPartsMods, + self.twPartsModsModel + )) self.btnCheckNewVersion.clicked.connect(lambda: self.check_new_version('button')) self.btnGithubLink.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.githubLink))) self.btnUpdateWeapons.clicked.connect(lambda: self.update_data('weapons')) @@ -79,6 +99,8 @@ def __init__(self): self.twRandom.setItemsExpandable(False) self.twRandom.customContextMenuRequested.connect(self.custom_tree_view_context_menu) self.twRandom.setContextMenuPolicy(Qt.CustomContextMenu) + self.twPartsWeapons.setModel(self.twMainModel) + self.twPartsMods.setModel(self.twPartsModsModel) self.tabWidgetMain.setCurrentWidget(self.tabMain) self.lblVersion.setText(os.environ.get('VERSION_NOW')) @@ -97,6 +119,8 @@ def __init__(self): self.update_json() self.check_new_version() + self.logger = logging.getLogger('TRND.main_window') + @pyqtSlot() def import_json(self): pathToJson = QFileDialog().getOpenFileName(self, 'Import JSON', '/', "json(*.json);; all(*.*)")[0] @@ -136,23 +160,28 @@ def update_json(self): self.twMain.setModel(self.twMainModel) self.twMain.setCurrentIndex(self.twMainModel.createIndex(0, 0)) - @pyqtSlot(str) - def find_and_display_weapons(self, query): + self.twPartsWeapons.setModel(self.twMainModel) + + self.twPartsModsModel.fillModel(self.jsonData['mods']) + self.twPartsMods.setModel(self.twPartsModsModel) + + @pyqtSlot(str, QTreeView, QStandardItemModel) + def find_and_display_rows(self, query, treeView_, standardModel_): if query == '': - self.twMain.setModel(self.twMainModel) + treeView_.setModel(standardModel_) return - if self.twMainModel.rowCount == 0: + if standardModel_.rowCount == 0: return searchModel = JsonModel() - for item in self.twMainModel.findItems(query, Qt.MatchContains): + for item in standardModel_.findItems(query, Qt.MatchContains): item_clone = item.clone() searchModel.appendRow(item_clone) JsonModel.copyItemWithChildren(item_clone, item) - self.twMain.setModel(searchModel) + treeView_.setModel(searchModel) @pyqtSlot() def create_random_weapon(self, randomJson_, key_, json_, isEmpty_=False): @@ -280,10 +309,44 @@ def start_update_restrict_timeout(self): self.updateTimeoutTimer.setUpdateInterval(1000) self.updateTimeoutTimer.start() + @pyqtSlot() + def notify_about_data_pages_update(self): + if not (lastPageEdit := check_data_pages_update()): + return + lastDataUpdateTime = dict() + for type_ in lastPageEdit: + try: + lastPageEdit[type_] = datetime.datetime.fromisoformat(lastPageEdit[type_].replace('Z', '')) + lastDataUpdateTime[type_] = datetime.datetime.fromisoformat(self.jsonData[type_ + 'LastUpdate']) + except KeyError: + pass + except ValueError: + pass + else: + if (self.isDataWeaponsUpdateQuestion and type_ == 'weapons')\ + or (self.isDataModsUpdateQuestion and type_ == 'mods'): + if lastPageEdit[type_] > lastDataUpdateTime[type_]: + typeTitle = 'оружия' if type_ == 'weapons' else 'модов' + res = QMessageBox.information(self, 'Новая версия ' + typeTitle, + 'Доступна новая версия ' + typeTitle + + '. Обновить?', + (QMessageBox.Ok | QMessageBox.Cancel)) + if type_ == 'weapons': + self.isDataWeaponsUpdateQuestion = False + elif type_ == 'mods': + self.isDataModsUpdateQuestion = False + + if res == QMessageBox.Ok: + self.tabWidgetMain.setCurrentWidget(self.tabData) + self.update_data(type_) + return + @pyqtSlot(str) def check_new_version(self, sender='timer'): if sender == 'button': self.start_update_restrict_timeout() + elif sender == 'timer': + self.notify_about_data_pages_update() response = utils.get_update_info(self.githubLinkLastRelease) @@ -295,19 +358,15 @@ def check_new_version(self, sender='timer'): str(response['error_msg'])) return - self.teUpdateChangeList.setText('Последняя версия: ' + response['data'][0]['tag_name']) - self.teUpdateChangeList.append('') - indent = ' ' + changelogStr = '' for version in response['data']: - self.teUpdateChangeList.append(version['name']) - if version['body']: - body = indent + version['body'].replace('\n', '\n' + indent) - else: - body = indent + 'Description not found' - self.teUpdateChangeList.append(body) + body = version['body'] or '        Описание отсутствует' + changelogStr += ''.join(['### ', version['name'], '\n', body, '\n\n']) + self.teUpdateChangeList.setMarkdown(changelogStr) if float(response['data'][0]['tag_name']) > float(os.environ.get('VERSION_NOW')): - self.tabWidgetMain.setTabText(2, self.tabUpdate.accessibleName() + ' (новая версия)') + self.tabWidgetMain.setTabText(self.tabWidgetMain.indexOf(self.tabUpdate), self.tabUpdate.accessibleName() + + '(новая версия)') if (sender == 'timer' and self.isUpdateQuestion) or self.tabWidgetMain.currentWidget() == self.tabUpdate: if sender == 'timer': message = 'Перейти на страницу обновления?' @@ -325,7 +384,7 @@ def check_new_version(self, sender='timer'): else: if sender == 'button': QMessageBox.information(self, 'Обновление', 'Установлена последняя версия.', QMessageBox.Ok) - self.tabWidgetMain.setTabText(2, self.tabUpdate.accessibleName()) + self.tabWidgetMain.setTabText(self.tabWidgetMain.indexOf(self.tabUpdate), self.tabUpdate.accessibleName()) @pyqtSlot() def update_app(self): @@ -412,7 +471,7 @@ def stop_process(): dlg.setCancelButton(btnCancel) btnCancel.hide() - dlg.setFixedSize(dlg.width()*1.5, dlg.height()) + dlg.setFixedSize(dlg.width() * 1.5, dlg.height()) dlg.canceled.connect(self.update_thread.stop) dlg.show() @@ -422,7 +481,7 @@ def stop_process(): self.update_thread.worker.progress.connect(dlg.setValue) self.update_thread.worker.progress.connect(lambda value: self.taskbarProgress.setValue( - (int(value) / int(total_length))*100)) + (int(value) / int(total_length)) * 100)) self.update_thread.worker.progress.connect(lambda value: lblText.setText(str(int(value / 1024)) + ' КБ / ' + total_length_display)) self.update_thread.thread.finished.connect(stop_process) @@ -451,6 +510,7 @@ def finish(main): main.lblProgress.setText("") main.update_json() main.taskbarProgress.hide() + self.notify_about_data_pages_update() if self.data_thread and self.data_thread.isRunning: self.data_thread.stop() @@ -528,7 +588,7 @@ def custom_tree_view_replace_random(self, index_): clickedRowText = self.twRandomModel.itemFromIndex(index_).text() pathToRootList = list() - while (item := self.twRandomModel.itemFromIndex(index_)) is not None\ + while (item := self.twRandomModel.itemFromIndex(index_)) is not None \ and (index_ := index_.parent()) is not None: pathToRootList.append(item.text()) pathToRootList.reverse() diff --git a/libs/wiki_parser.py b/libs/wiki_parser.py index e3809f1..e83e03c 100644 --- a/libs/wiki_parser.py +++ b/libs/wiki_parser.py @@ -1,4 +1,6 @@ import datetime +import json +import logging import math import time @@ -7,6 +9,35 @@ from libs import utils +def check_data_pages_update(): + URL = 'https://escapefromtarkov.fandom.com/ru/api.php?' + PARAMS = { + "action": "query", + "prop": "revisions", + "titles": "Оружие|Оружейные части и моды", + "rvprop": "timestamp", + "formatversion": "2", + "format": "json" + } + + try: + response = requests.get(url=URL, params=PARAMS) + response.raise_for_status() + response = response.json() + except requests.exceptions.RequestException: + response = None + except json.JSONDecodeError: + response = None + + try: + lastPageEdit = dict() + lastPageEdit['weapons'] = response['query']['pages'][0]['revisions'][0]['timestamp'] + lastPageEdit['mods'] = response['query']['pages'][1]['revisions'][0]['timestamp'] + except KeyError: + lastPageEdit = '' + return lastPageEdit + + def get_data_from_wiki(worker_, dict_): def showError(): worker_.error.emit( @@ -19,6 +50,8 @@ def showError(): urlMods = 'https://escapefromtarkov.fandom.com/ru/wiki/%D0%9E%D1%80%D1%83%D0%B6%D0%B5%D0%B9%D0%BD%D1%8B%D0%B5_' \ '%D1%87%D0%B0%D1%81%D1%82%D0%B8_%D0%B8_%D0%BC%D0%BE%D0%B4%D1%8B ' + logger = logging.getLogger('TRND.wiki_parser') + weapons = 'weapons' mods = 'mods' modsConflicts = 'modsConflicts' @@ -33,8 +66,13 @@ def showError(): secondLevelFormatStr = '%8s%s' thirdLevelFormatStr = '%12s%s' - outDict = utils.validate_data(dict_['jsonData']) + outDict = dict_['jsonData'].copy() + outDict.pop(dict_['type'], None) + if dict_['type'] == mods: + outDict.pop(modsConflicts, None) + outDict = utils.validate_data(outDict) + logger.info("Started " + dict_['type'] + " update") downloadTime = time.perf_counter() if dict_['type'] == weapons or dict_['type'] == mods: minF = 0 @@ -58,6 +96,7 @@ def showError(): html = requests.get(queryPage) html.raise_for_status() except requests.exceptions.RequestException as err: + logger.error(err) showError() return @@ -103,6 +142,7 @@ def showError(): r = requests.get(site + a_.get('href')) r.raise_for_status() except requests.exceptions.RequestException as err: + logger.error(err) showError() return @@ -180,6 +220,8 @@ def showError(): if modLinkStr not in outDict[dict_['type']][weaponNameStr]: outDict[dict_['type']][weaponNameStr].append(modLinkStr) + logger.info(dict_['type'] + " update completed") + elapsed = time.perf_counter() - downloadTime elapsedStr = ("--- %.f minutes %.f seconds ---" % ((elapsed / 60), elapsed % 60)) worker_.display_text.emit(elapsedStr) diff --git a/main.py b/main.py index c8b0cf0..f63eadd 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,23 @@ +import datetime import os import platform import sys +import logging +from pathlib import Path from PyQt5 import QtGui from PyQt5.QtCore import QTranslator from PyQt5.QtWidgets import QApplication from qt_material import apply_stylesheet -from libs.window import MyWindow +from libs.main_window import MainWindow + + +def suppress_qt_warnings(): + os.environ["QT_DEVICE_PIXEL_RATIO"] = "0" + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + os.environ["QT_SCREEN_SCALE_FACTORS"] = "1" + os.environ["QT_SCALE_FACTOR"] = "1" def init_environ(): @@ -16,26 +26,61 @@ def init_environ(): # Adding path to internal json if platform.system() == 'Windows': - os.environ['DATAFOLDER'] = os.environ.get('APPDATA') + '\\TRND\\' - os.environ['DATAFILE'] = os.environ.get('APPDATA') + '\\TRND\\data.json' + os.environ['DATAFOLDER'] = os.path.join(os.environ.get('APPDATA'), 'TRND') else: - os.environ['DATAFOLDER'] = os.environ.get('HOME') + '/.trnd/' - os.environ['DATAFILE'] = os.environ.get('HOME') + '/.trnd/data.json' + os.environ['DATAFOLDER'] = os.path.join(os.environ.get('HOME'), '.trnd') + + os.environ['DATAFILE'] = os.path.join(os.environ.get('DATAFOLDER'), 'data.json') + os.environ['LOGSFOLDER'] = os.path.join(os.environ.get('DATAFOLDER'), 'logs') if not os.path.exists(os.environ.get('DATAFOLDER')): os.makedirs(os.environ.get('DATAFOLDER')) - os.environ['VERSION_NOW'] = '0.16' + os.environ['VERSION_NOW'] = '0.17' -def suppress_qt_warnings(): - os.environ["QT_DEVICE_PIXEL_RATIO"] = "0" - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" - os.environ["QT_SCREEN_SCALE_FACTORS"] = "1" - os.environ["QT_SCALE_FACTOR"] = "1" +def init_logger(): + logger = logging.getLogger('TRND') + logger.setLevel(logging.INFO) + + formatter_ = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + logsPath = os.path.abspath(os.environ.get('LOGSFOLDER')) + filename = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + '.log' + fullPath = os.path.join(logsPath, filename) + + Path(logsPath).mkdir(parents=True, exist_ok=True) + filesCount = len(os.listdir(logsPath)) -def apply_theme(app): + def clear_dir(directory): + directory = Path(directory) + for item in directory.iterdir(): + if item.is_dir(): + clear_dir(item) + else: + item.unlink() + + if filesCount > 10: + clear_dir(logsPath) + + try: + fileHandler = logging.FileHandler(fullPath) + except PermissionError: + pass + else: + fileHandler.setFormatter(formatter_) + + logger.addHandler(fileHandler) + logger.info('Program started') + + +def init_translator(app): + translator = QTranslator(app) + translator.load("data/qtbase_ru.qm") + app.installTranslator(translator) + + +def init_theme(app): QtGui.QFontDatabase.addApplicationFont("data/Roboto-Regular.ttf") apply_stylesheet(app, theme="data/tarkov_theme.xml") with open('data/additional_style.css') as file: @@ -45,16 +90,14 @@ def apply_theme(app): def main(): suppress_qt_warnings() init_environ() + init_logger() app = QApplication(sys.argv) - translator = QTranslator(app) - translator.load("data/qtbase_ru.qm") - - app.installTranslator(translator) - apply_theme(app) + init_translator(app) + init_theme(app) - window = MyWindow() + window = MainWindow() window.show() sys.exit(app.exec_())