import os import sys import copy import enum import json import time import random import shutil import zipfile from random import random as rand from typing import List, Tuple, Union from PyQt6 import QtGui, QtCore, QtWidgets, QtMultimedia, QtMultimediaWidgets class InteractiveVariable: """ 互动节点的变量 """ def __init__( self, name: str, var_id: str, var_value: int, show: bool = False, random: bool = False, ): """ Args: name (str) : 变量名 var_id (str) : 变量 id var_value (int) : 变量的值 show (bool) : 是否显示 random (bool) : 是否为随机值(1-100) """ self.__var_id = var_id self.__var_value = var_value self.__name = name self.__is_show = show self.__random = random def get_id(self) -> str: return self.__var_id def refresh_value(self) -> None: """ 刷新变量数值 """ if self.is_random(): self.__var_value = int(rand(0, 100)) def get_value(self) -> int: return self.__var_value def is_show(self) -> bool: return self.__is_show def is_random(self) -> bool: return self.__random def get_name(self) -> str: return self.__name def __str__(self): return f"{self.__name} {self.__var_value}" class InteractiveButtonAlign(enum.Enum): """ 按钮的文字在按钮中的位置 ``` text ----- |xxx|----o (TEXT_LEFT) ----- ----- o----|xxx| (TEXT_RIGHT) ----- ---------- |XXXXXXXX| (DEFAULT) ---------- ``` - DEFAULT - TEXT_UP - TEXT_RIGHT - TEXT_DOWN - TEXT_LEFT """ DEFAULT = 0 TEXT_UP = 1 TEXT_RIGHT = 2 TEXT_DOWN = 3 TEXT_LEFT = 4 class InteractiveButton: """ 互动视频节点按钮类 """ def __init__( self, text: str, x: int, y: int, align: Union[InteractiveButtonAlign, int] = InteractiveButtonAlign.DEFAULT, ): """ Args: text (str) : 文字 x (int) : x 轴 y (int) : y 轴 align (InteractiveButtonAlign | int): 按钮的文字在按钮中的位置 """ self.__text = text self.__pos = (x, y) if isinstance(align, InteractiveButtonAlign): align = align.value self.__align = align def get_text(self) -> str: return self.__text def get_align(self) -> int: return self.__align # type: ignore def get_pos(self) -> Tuple[int, int]: return self.__pos def __str__(self): return f"{self.__text} {self.__pos}" class InteractiveJumpingCondition: """ 节点跳转的公式,只有公式成立才会跳转 """ def __init__(self, var: List[InteractiveVariable] = [], condition: str = "True"): """ Args: var (List[InteractiveVariable]): 所有变量 condition (str) : 公式 """ self.__vars = var self.__command = condition def get_result(self) -> bool: """ 计算公式获得结果 Returns: bool: 是否成立 """ if self.__command == "": return True command = copy.copy(self.__command) for var in self.__vars: var_name = var.get_id() var_value = var.get_value() command = command.replace(var_name, str(var_value)) command = command.replace("&&", " and ") command = command.replace("||", " or ") command = command.replace("!", " not ") command = command.replace("===", "==") command = command.replace("!==", "!=") command = command.replace("true", "True") command = command.replace("false", "False") return eval(command) def __str__(self): return f"{self.__command}" class InteractiveJumpingCommand: """ 节点跳转对变量的操作 """ def __init__(self, var: List[InteractiveVariable] = [], command: str = ""): """ Args: var (List[InteractiveVariable]): 所有变量 condition (str) : 公式 """ self.__vars = var self.__command = command def run_command(self) -> List["InteractiveVariable"]: """ 执行操作 Returns: List[InteractiveVariable] """ if self.__command == "": return self.__vars for code in self.__command.split(";"): var_name_ = code.split("=")[0] var_new_value = code.split("=")[1] for var in self.__vars: var_name = var.get_id() var_value = var.get_value() var_new_value = var_new_value.replace(var_name, str(var_value)) var_new_value_calc = eval(var_new_value) for var in self.__vars: if var.get_id() == var_name_: var._InteractiveVariable__var_value = var_new_value_calc # type: ignore return self.__vars class InteractiveNodeJumpingType(enum.Enum): """ 对下一节点的跳转的方式 - ASK : 选择 - DEFAULT: 跳转到默认节点 - READY : 选择(只有一个选择) """ READY = 1 DEFAULT = 0 ASK = 2 class Button: def __init__(self, id_, pos, text, condition, command): # A class that provides the button model. self.node_id = id_ self.pos = pos self.text = text self.condition = condition self.command = command # 什么?别问我为什么不用 TypedDict / dataclass def __str__(self) -> str: return f"{self.pos} {self.text} {self.condition} {self.command}" class ButtonLabel(QtWidgets.QLabel): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent=parent) self.setObjectName(str(time.time())) def prep_text(self, text: str, x: int, y: int): self.setText(text) self.setWordWrap(True) self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) font = QtGui.QFont() font.setPointSize(12) font.setBold(True) self.setFont(font) rect = QtCore.QRect(x, y, 200, 50) self.setGeometry(rect) self.setStyleSheet( "border-width: 5px;\ border-style: solid;\ border-color: rgb(100, 100, 100);\ background-color: rgb(50, 50, 50);\ color: rgb(255, 255, 255);" ) self.raise_() return self class MPlayer(object): def setup(self, Form): # UI Form.setObjectName("Form") Form.resize(800, 600) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) Form.setSizePolicy(sizePolicy) Form.setMinimumSize(QtCore.QSize(800, 650)) Form.setMaximumSize(QtCore.QSize(800, 650)) Form.setBaseSize(QtCore.QSize(800, 650)) Form.setWindowTitle("MPlayer") self.win: QtWidgets.QWidget = Form self.player = QtMultimediaWidgets.QVideoWidget(Form) self.player.setGeometry(QtCore.QRect(0, 0, 800, 450)) self.player.setObjectName("player") self.mediaplayer = QtMultimedia.QMediaPlayer() self.mediaplayer.setVideoOutput(self.player) self.audio_output = QtMultimedia.QAudioOutput() self.audio_output.setVolume(0.0) self.mediaplayer.setAudioOutput(self.audio_output) self.slider = QtWidgets.QSlider(Form) self.slider.setGeometry(QtCore.QRect(120, 455, 571, 22)) self.slider.setOrientation(QtCore.Qt.Orientation.Horizontal) self.slider.setObjectName("slider") self.slider.setValue(100) self.pp = QtWidgets.QPushButton(Form) self.pp.setGeometry(QtCore.QRect(0, 450, 113, 32)) self.pp.setObjectName("pp") self.pushButton = QtWidgets.QPushButton(Form) self.pushButton.setGeometry(QtCore.QRect(0, 525, 113, 32)) self.pushButton.setObjectName("pushButton") self.node = QtWidgets.QLabel(Form) self.node.setGeometry(QtCore.QRect(120, 530, 191, 16)) self.node.setObjectName("node") self.info = QtWidgets.QLabel(Form) self.info.setGeometry(QtCore.QRect(320, 520, 471, 36)) self.info.setWordWrap(True) font = QtGui.QFont() font.setBold(True) font.setWeight(75) self.info.setFont(font) self.info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.info.setObjectName("info") self.pushButton_2 = QtWidgets.QPushButton(Form) self.pushButton_2.setGeometry(QtCore.QRect(0, 560, 113, 32)) self.pushButton_2.setObjectName("pushButton_2") self.lineEdit = QtWidgets.QLineEdit(Form) self.lineEdit.setEnabled(True) self.lineEdit.setGeometry(QtCore.QRect(120, 565, 561, 21)) self.lineEdit.setText("") self.lineEdit.setObjectName("lineEdit") self.lineEdit.setPlaceholderText("Type in the path to your ivi file. ") self.pushButton_3 = QtWidgets.QPushButton(Form) self.pushButton_3.setGeometry(QtCore.QRect(690, 560, 108, 32)) self.pushButton_3.setObjectName("pushButton_3") self.pushButton_4 = QtWidgets.QPushButton(Form) self.pushButton_4.setGeometry(QtCore.QRect(0, 485, 113, 32)) self.pushButton_4.setObjectName("pushButton_4") self.horizontalSlider = QtWidgets.QSlider(Form) self.horizontalSlider.setGeometry(QtCore.QRect(120, 490, 571, 22)) self.horizontalSlider.setOrientation(QtCore.Qt.Orientation.Horizontal) self.horizontalSlider.setObjectName("horizontalSlider") self.label = QtWidgets.QLabel(Form) self.label.setGeometry(QtCore.QRect(699, 449, 81, 31)) self.label.setObjectName("label") self.label_2 = QtWidgets.QLabel(Form) self.label_2.setGeometry(QtCore.QRect(700, 490, 60, 16)) self.label_2.setObjectName("label_2") # Slot & String self.pp.setEnabled(False) self.pushButton.setEnabled(False) self.retranslateUi(Form) self.win.closeEvent = self.on_close_check self.pushButton_2.clicked.connect(self.open_ivi) self.pushButton_3.clicked.connect(self.close_ivi) self.pushButton_4.clicked.connect(self.sound_on_off_event) self.pp.clicked.connect(self.pp_button) self.horizontalSlider.valueChanged.connect(self.volume_change_event) self.slider.sliderReleased.connect(self.position_change_event) self.slider.sliderPressed.connect(self.position_start_change_event) self.pushButton.clicked.connect(self.back_to_previous) # InteractiveVariables self.current_node = 0 self.variables: List[InteractiveVariable] = [] self.state_log = [] self.graph = None self.choice_buttons: List[Button] = [] self.choice_labels: List[ButtonLabel] = [] # Video Play Variables & Functions self.temp_dir = "" self.is_draging_slider = False self.is_stoping = False self.win.startTimer(100) self.has_end = False self.final_position = -1 # Timer & Refresh def timerEvent(*args, **kwargs): # 创建要跳转的节点 if self.has_end: if len(self.choice_labels) != 0: for lbl in self.choice_labels: lbl.raise_() self.player.lower() else: children = self.graph[str(self.current_node)]["sub"] if len(children) == 0: # 已结束 pass else: # 跳转类型 if ( self.graph[str(children[0])]["jump_type"] == InteractiveNodeJumpingType.DEFAULT.value ): # 直接跳转 for node_id in children: btn = Button( node_id, [0, 0], "", self.graph[str(node_id)]["condition"], self.graph[str(node_id)]["command"], ) condition = InteractiveJumpingCondition( self.variables, btn.condition ) if condition.get_result(): # 可以跳转 native_command = InteractiveJumpingCommand( self.variables, btn.command ) self.variables = native_command.run_command() btn_id = btn.node_id self.set_source(self.graph[str(btn_id)]["cid"]) self.current_node = btn.node_id self.volume_change_event() title = self.graph[str(node_id)]["title"] self.node.setText(f"(当前节点: {title})") break else: # 进行选择 def get_info(node_id: int): return self.graph[str(node_id)] cnt = 0 for idx, child in enumerate(children): pos_x = cnt * 200 pos_y = 600 cur_info = get_info(child) # 生成 Button 对象 self.choice_buttons.append( Button( child, [pos_x, pos_y], cur_info["button"]["text"], cur_info["condition"], cur_info["command"], ) ) # 生成 ButtonLabel 对象 if cur_info["button"]["pos"][0] == None: if idx != 0: previous_info = get_info(children[idx - 1]) curtext, previoustext = ( cur_info["button"]["text"], previous_info["button"]["text"], ) if curtext[2:] == previoustext[2:]: # 可确定与上一个按钮同一个位置(即概率按钮) # 不再生成单独的 label self.choice_buttons[-1].pos[0] -= 200 continue cnt += 1 lbl = ButtonLabel(self.win) lbl.prep_text( cur_info["button"]["text"], pos_x, pos_y ) lbl.show() self.choice_labels.append(lbl) continue if idx != 0: previous_info = get_info(children[idx - 1]) curpos, previouspos = ( cur_info["button"]["pos"], previous_info["button"]["pos"], ) if (abs(curpos[0] - previouspos[0]) <= 5) and ( abs(curpos[1] - previouspos[1]) <= 5 ): # 可确定与上一个按钮同一个位置(即概率按钮) # 不再生成单独的 label self.choice_buttons[-1].pos[0] -= 200 else: # 生成 label cnt += 1 lbl = ButtonLabel(self.win) lbl.prep_text( cur_info["button"]["text"], pos_x, pos_y ) lbl.show() self.choice_labels.append(lbl) else: # 生成 label cnt += 1 lbl = ButtonLabel(self.win) lbl.prep_text( cur_info["button"]["text"], pos_x, pos_y ) lbl.show() self.choice_labels.append(lbl) pass add_space = int((800 - cnt * 200) / 2) for idx, lbl in enumerate(self.choice_labels): lbl.setGeometry( QtCore.QRect( lbl.geometry().left() + add_space, lbl.geometry().top(), lbl.geometry().width(), lbl.geometry().height(), ) ) for btn in self.choice_buttons: btn.pos[0] += add_space # 处理进度条 if self.is_draging_slider: return if self.mediaplayer.duration() == 0: self.slider.setValue(100) self.label.setText("--:--/--:--") return if ( (self.mediaplayer.duration() // 1000) == ((self.mediaplayer.position() // 1000)) ) and (not self.has_end): self.has_end = True self.mediaplayer.pause() self.final_position = self.mediaplayer.position() self.mediaplayer.setAudioOutput( QtMultimedia.QAudioOutput().setVolume(0) ) self.slider.setValue(100) duration = self.mediaplayer.duration() // 1000 duration_sec = duration % 60 duration_min = duration // 60 if duration_sec < 10: duration_sec = "0" + str(duration_sec) if duration_min < 10: duration_min = "0" + str(duration_min) self.label.setText( f"{duration_min}:{duration_sec}/{duration_min}:{duration_sec}" ) self.player.lower() for lbl in self.choice_labels: lbl.raise_() return elif self.has_end: self.has_end = True self.slider.setValue(100) self.mediaplayer.setPosition(self.final_position) self.mediaplayer.setAudioOutput( QtMultimedia.QAudioOutput().setVolume(0) ) duration = self.mediaplayer.duration() // 1000 duration_sec = duration % 60 duration_min = duration // 60 if duration_sec < 10: duration_sec = "0" + str(duration_sec) if duration_min < 10: duration_min = "0" + str(duration_min) self.label.setText( f"{duration_min}:{duration_sec}/{duration_min}:{duration_sec}" ) self.player.lower() for lbl in self.choice_labels: lbl.raise_() return else: self.has_end = False self.choice_buttons = [] for lbl in self.choice_labels: lbl.hide() self.choice_labels = [] self.last_position = self.mediaplayer.position() self.slider.setValue( int(self.mediaplayer.position() / self.mediaplayer.duration() * 100) ) duration = self.mediaplayer.duration() // 1000 position = self.mediaplayer.position() // 1000 duration_sec = duration % 60 duration_min = duration // 60 position_sec = position % 60 position_min = position // 60 if duration_sec < 10: duration_sec = "0" + str(duration_sec) if duration_min < 10: duration_min = "0" + str(duration_min) if position_sec < 10: position_sec = "0" + str(position_sec) if position_min < 10: position_min = "0" + str(position_min) self.label.setText( f"{position_min}:{position_sec}/{duration_min}:{duration_sec}" ) # 将选择的按钮置于最上层 for lbl in self.choice_labels: lbl.raise_() self.win.timerEvent = timerEvent # Click & Jump def mouseReleaseEvent(event: QtGui.QMouseEvent): pos = event.position() pos = [pos.x(), pos.y()] for var in self.variables: if var.is_random(): var._InteractiveVariable__var_value = random.random() * 100 for btn in self.choice_buttons: if ( (pos[0] - btn.pos[0] <= 200) and (pos[0] - btn.pos[0] >= 0) and (pos[1] - btn.pos[1] <= 50) and (pos[1] - btn.pos[1] >= 0) ): condition = InteractiveJumpingCondition( self.variables, btn.condition ) if condition.get_result(): # 可以跳转 native_command = InteractiveJumpingCommand( self.variables, btn.command ) self.variables = native_command.run_command() btn_id = btn.node_id self.set_source(self.graph[str(btn_id)]["cid"]) self.current_node = btn.node_id self.volume_change_event() title = self.graph[str(btn.node_id)]["title"] self.node.setText(f"(当前节点: {title})") break self.win.mouseReleaseEvent = mouseReleaseEvent def start_playing(self): self.mediaplayer.play() self.is_stoping = False def stop_playing(self): self.mediaplayer.stop() self.is_stoping = True def pause_playing(self): self.mediaplayer.pause() self.is_stoping = True def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate self.pp.setText(_translate("Form", "Pause")) self.pushButton.setText(_translate("Form", "<- Previous")) self.node.setText(_translate("Form", "(当前节点: 无)")) self.info.setText(_translate("Form", "视频标题(BVID)")) self.pushButton_2.setText(_translate("Form", "Open")) self.pushButton_3.setText(_translate("Form", "Close")) self.pushButton_4.setText(_translate("Form", "Sound: Off")) self.label.setText(_translate("Form", "--:--/--:--")) self.label_2.setText(_translate("Form", "0")) def set_source(self, cid: int): wintitle = "MPlayer" for var in self.variables: if var.is_show(): wintitle += f" - {var.get_name()}: {int(var.get_value())}" for lbl in self.choice_labels: lbl.hide() self.choice_labels = [] self.choice_buttons = [] self.win.setWindowTitle(wintitle) self.state_log.append({"cid": cid, "vars": copy.deepcopy(self.variables)}) self.has_end = False self.mediaplayer.setAudioOutput( QtMultimedia.QAudioOutput().setVolume(self.horizontalSlider.value() / 100) ) self.stop_playing() self.pp.setText("Pause") dest = self.temp_dir + str(cid) + ".mp4" self.mediaplayer.setSource(QtCore.QUrl(dest)) self.mediaplayer.setPosition(0) if self.mediaplayer.duration() <= 7: self.mediaplayer.setPosition(self.mediaplayer.duration()) self.start_playing() def extract_ivi(self, path: str): curtime = str(time.time()) try: os.mkdir(".mplayer") except: pass os.mkdir(".mplayer/" + curtime) self.temp_dir = ".mplayer/" + curtime + "/" ivi = zipfile.ZipFile(path) ivi.extractall(self.temp_dir) bilivideo_parser = json.JSONDecoder() self.node.setText("(当前节点: 视频主节点)") self.info.setText( bilivideo_parser.decode( open(self.temp_dir + "bilivideo.json", "r", encoding="utf-8").read() )["title"] + "(" + bilivideo_parser.decode( open(self.temp_dir + "bilivideo.json", "r", encoding="utf-8").read() )["bvid"] + ")" ) self.graph = json.load( open(self.temp_dir + "ivideo.json", "r", encoding="utf-8") ) self.current_node = 1 variables = self.graph["1"]["vars"] for var in variables: self.variables.append( InteractiveVariable( var["name"], var["id"], var["value"], var["show"], var["random"] ) ) self.set_source(self.graph["1"]["cid"]) self.volume_change_event() def close_ivi(self): self.player.hide() self.player = QtMultimediaWidgets.QVideoWidget(self.win) self.player.setGeometry(QtCore.QRect(0, 0, 800, 450)) self.player.setObjectName("player") self.player.show() self.current_node = 0 self.variables = [] self.state_log = [] self.choice_buttons = [] for lbl in self.choice_labels: lbl.hide() self.choice_labels = [] self.graph = None self.stop_playing() self.pp.setText("Pause") self.has_end = False self.mediaplayer = QtMultimedia.QMediaPlayer() # Clear the multimedia source self.mediaplayer.setVideoOutput(self.player) self.mediaplayer.setAudioOutput(QtMultimedia.QAudioOutput()) self.volume_change_event() shutil.rmtree(self.temp_dir) while True: if not os.path.exists(self.temp_dir): break try: os.rmdir(".mplayer") except: # Not empty pass self.temp_dir = "" self.node.setText("(当前节点: 无)") self.info.setText("视频标题(BVID)") self.win.setWindowTitle("MPlayer") self.lineEdit.setText("") self.pp.setEnabled(False) self.pushButton.setEnabled(False) def open_ivi(self): if self.current_node != 0: return try: if self.lineEdit.text() != "": self.extract_ivi(self.lineEdit.text()) else: dialog = QtWidgets.QFileDialog() filename, _ = dialog.getOpenFileName( self.win, "Choose an 'ivi' file to open. ", filter="Bilibili Interactive Video (*.ivi)", ) self.extract_ivi(filename) self.lineEdit.setText(filename) self.pp.setEnabled(True) self.pushButton.setEnabled(True) except Exception as e: warning = QtWidgets.QMessageBox() warning.warning(self.win, "Oops...", str(e)) def volume_change_event(self): if self.horizontalSlider.value() == 0: self.pushButton_4.setText("Sound: Off") else: self.pushButton_4.setText("Sound: On") position = self.mediaplayer.position() if (not self.has_end) or (not self.is_stoping): pass else: self.pause_playing() volume = self.horizontalSlider.value() self.label_2.setText(str(volume)) self.audio_output.setVolume(float(volume / 100)) self.mediaplayer.setAudioOutput(self.audio_output) if (not self.has_end) or (not self.is_stoping): pass else: self.mediaplayer.setPosition(position) self.start_playing() def position_start_change_event(self): self.mediaplayer.pause() self.is_draging_slider = True def position_change_event(self): volume = self.slider.value() if volume != 100 and self.has_end: self.slider.setValue(100) return self.mediaplayer.setPosition(int(self.mediaplayer.duration() * volume / 100)) if not self.is_stoping: self.start_playing() self.is_draging_slider = False def sound_on_off_event(self): if "on" in self.pushButton_4.text().lower(): self.pushButton_4.setText("Sound: Off") curpos = self.mediaplayer.position() self.mediaplayer.pause() volume = self.horizontalSlider.value() self.label_2.setText(str(volume)) self.audio_output = QtMultimedia.QAudioOutput() self.audio_output.setVolume(0.0) self.mediaplayer.setAudioOutput(self.audio_output) self.mediaplayer.setPosition(curpos) if not self.is_stoping: self.start_playing() self.horizontalSlider.setSliderPosition(0) else: self.pushButton_4.setText("Sound: On") curpos = self.mediaplayer.position() self.mediaplayer.pause() volume = self.horizontalSlider.value() self.label_2.setText(str(volume)) self.audio_output = QtMultimedia.QAudioOutput() self.audio_output.setVolume(1.0) self.mediaplayer.setAudioOutput(self.audio_output) self.mediaplayer.setPosition(curpos) if not self.is_stoping: self.start_playing() self.horizontalSlider.setSliderPosition(100) def pp_button(self): if self.is_stoping: self.start_playing() self.pp.setText("Pause") else: self.pause_playing() self.pp.setText("Play") def on_close_check(self, event): if self.current_node != 0: reply = QtWidgets.QMessageBox.question( self.win, "WARNING", "IVI file is playing. Are you sure want to exit? ", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No, ) if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.close_ivi() event.accept() else: event.ignore() else: event.accept() def back_to_previous(self): if len(self.state_log) < 2: QtWidgets.QMessageBox.warning( self.win, "WTF???", "MPlayer can't find the previous node. \nMaybe there's not any node or only one node?", ) return new_cid = copy.deepcopy(self.state_log[-2]["cid"]) new_vars = copy.deepcopy(self.state_log[-2]["vars"]) self.state_log.pop() for key in self.graph.keys(): if self.graph[key]["cid"] == new_cid: new_node_id = int(key) self.current_node = new_node_id self.variables = new_vars self.set_source(new_cid) self.state_log.pop() title = self.graph[str(new_node_id)]["title"] self.node.setText(f"(当前节点: {title})") self.volume_change_event() return def main(): app = QtWidgets.QApplication(sys.argv) win = QtWidgets.QMainWindow() ui = MPlayer() ui.setup(win) win.show() sys.exit(app.exec()) def prepopen(path: str): app = QtWidgets.QApplication(sys.argv) win = QtWidgets.QMainWindow() ui = MPlayer() ui.setup(win) ui.lineEdit.setText(path) ui.open_ivi() win.show() sys.exit(app.exec()) if __name__ == "__main__": main()