>= 1 + else: + self.drives["ROOT"] = "/" + self.write_u32(len(self.drives)) + for d in self.drives: try: - Goldleaf.write(PFS0.read_nca(Goldleaf.ticket_index)) - break + self.write_string(drive_labels[d]) + except KeyError: + self.write_string(d) + self.write_string(d) + elif self.is_id(GoldleafCommandId.GetEnvironmentPaths): + env_paths = {x:os.path.expanduser("~/"+x) for x in ["Desktop", "Documents"]} + for arg in sys.argv[1:]: + folder = os.path.abspath(arg) + if os.path.isfile(folder): + folder = os.path.dirname(folder) + env_paths[os.path.basename(folder)] = folder + env_paths = {x:env_paths[x] for x in env_paths if os.path.exists(env_paths[x])} + self.write_u32(len(env_paths)) + for env in env_paths: + env_paths[env] = env_paths[env].replace("\\", "/") + self.write_string(env) + if env_paths[env][1:3] != ":/": + env_paths[env] = "ROOT:" + env_paths[env] + self.write_string(env_paths[env]) + elif self.is_id(GoldleafCommandId.GetPathType): + ptype = 0 + path = self.read_path() + if os.path.isfile(path): + ptype = 1 + elif os.path.isdir(path): + ptype = 2 + self.write_u32(ptype) + elif self.is_id(GoldleafCommandId.ListDirectories): + path = self.read_path() + ents = [x for x in os.listdir(path) if os.path.isdir(os.path.join(path, x))] + n_ents = [] + for e in ents: + try: + test = os.listdir(os.path.join(path, e)) + n_ents.append(e) except: pass - - - elif Goldleaf.is_id(CommandId.Finish) and Goldleaf.magic_ok(): - set_progress(100,100) - complete_install() - sys.exit() - except: - pass - return 0 - -def init_goldleaf_usb_install(): - Goldleaf.reset() - PFS0.reset() - for file in selected_files: - try: - set_cur_nsp(os.path.basename(file)) - Goldleaf.nsp_path = str(file) - Goldleaf.connect_usb() - Goldleaf.Goldleaf_USB() - except Exception as e: - if is_logging: - logging.error(e, exc_info=True) - throw_error(0) - try: - usb.util.dispose_resources(dev) - dev.reset() - except: - pass - sys.exit() - usb.util.dispose_resources(dev) - dev.reset() + self.write_u32(len(n_ents)) + for name in n_ents: + self.write_string(name) + elif self.is_id(GoldleafCommandId.ListFiles): + self.fw_status = self.FW_NOSTATUS + if is_installing: + complete_goldleaf_transfer() + path = self.read_path() + ents = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))] + if not allow_access_non_nsp: + len_nsps = 0 + for f in ents: + if f.lower().endswith('.nsp'): + len_nsps = len_nsps+1 + self.write_u32(len_nsps) + for name in ents: + if name.lower().endswith('.nsp'): + self.write_string(name) + else: + self.write_u32(len(ents)) + for name in ents: + self.write_string(name) + elif self.is_id(GoldleafCommandId.GetFileSize): + path = self.read_path() + self.write_u64(os.path.getsize(path)) + elif self.is_id(GoldleafCommandId.FileRead): + can_read = True + offset = self.read_u64() + size = self.read_u64() + path = self.read_path() + if not os.path.basename(path).lower().endswith('.nsp'): + if allow_access_non_nsp: + can_read = True + else: + can_read = False + if can_read: + with open(path, "rb") as f: + f.seek(offset) + data = f.read(size) + self.write_u64(len(data)) + self.write(data) + try: + if self.fw_status != self.FW_DENIED: + complete_loading() + set_cur_nsp(str(os.path.basename(path))) + set_progress(int(offset), int(os.path.getsize(path))) + elapsed_time = time.time() - start_time + if elapsed_time >= 1: + set_cur_transfer_rate(int(offset) - last_transfer_rate) + set_last_transfer_rate(int(offset)) + set_start_time() + else: + complete_goldleaf_transfer() + except: + pass + else: + logging.debug("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).") + print("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).") + cancel_task() + sys.exit() + elif self.is_id(GoldleafCommandId.FileWrite): + offset = self.read_u64() + size = self.read_u64() + path = self.read_path() + data = self.read(size) + can_write = False + if self.fw_status == self.FW_NOSTATUS: + get_response_qmessage(1) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + self.fw_status = self.FW_ACCEPTED + can_write = True + else: + self.fw_status = self.FW_DENIED + elif self.fw_status == self.FW_ACCEPTED: + can_write = True + if can_write: + cont = bytearray() + try: + with open(path, "rb") as f: + cont=bytearray(f.read()) + except FileNotFoundError: + pass + cont[offset:offset + size] = data + with open(path, "wb") as f: + f.write(cont) + reset_response() + elif self.is_id(GoldleafCommandId.CreateFile): + path = self.read_path() + get_response_qmessage(2) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + open(path, "a").close() + reset_response() + elif self.is_id(GoldleafCommandId.CreateDirectory): + path = self.read_path() + get_response_qmessage(3) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + try: + os.mkdir(path) + except os.FileExistsError: + pass + reset_response() + elif self.is_id(GoldleafCommandId.DeleteFile): + path = self.read_path() + get_response_qmessage(4) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + os.remove(path) + reset_response() + elif self.is_id(GoldleafCommandId.DeleteDirectory): + path = self.read_path() + get_response_qmessage(5) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + shutil.rmtree(path) + reset_response() + elif self.is_id(GoldleafCommandId.RenameFile): + path = self.read_path() + new_name = self.read_string() + get_response_qmessage(6) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + os.rename(path, f"{os.path.dirname(path)}/{new_name}") + reset_response() + elif self.is_id(GoldleafCommandId.RenameDirectory): + path = self.read_path() + new_name = self.read_path() + get_response_qmessage(6) + while not haveresponse and global_dev is not None: + time.sleep(1) + if qresponse: + os.rename(path, new_name) + reset_response() + elif self.is_id(GoldleafCommandId.GetDriveTotalSpace): + path = self.read_path() + disk = os.statvfs(path) + totalBytes = float(disk.f_bsize*disk.f_blocks) + self.write_u64(int(totalspace)) + elif self.is_id(GoldleafCommandId.GetDriveFreeSpace): + path = self.read_path() + disk = os.statvfs(path) + totalFreeSpace = float(disk.f_bsize*disk.f_bfree) + self.write_u64(int(totalFreeSpace)) sys.exit() # Tinfoil Network netrlist = [] - def reset_netrlist(): global netrlist netrlist = None netrlist = [] - def append_netrlist(v, v2): global netrlist netrlist.append((v, v2)) - -class RangeHTTPRequestHandler(SimpleHTTPRequestHandler): +class TinfoilNetwork: + def init(self): + reset_netrlist() + accepted_extension = ('.nsp') + hostPort = random.randint(26490,26999) + target_ip = switch_ip + hostIp = host_ip + target_path = str(selected_dir).strip() + baseUrl = hostIp + ':' + str(hostPort) + '/' + directory = target_path + file_list_payload = '' + for file in [file for file in next(os.walk(target_path))[2] if file.endswith(accepted_extension)]: + for y in selected_files: + if str(file).find(os.path.basename(y)) != -1: + n = random.randint(1,10000000) + fake_file = str(n) + ".nsp" + append_netrlist(fake_file, str(y)) + file_list_payload += baseUrl + fake_file + '\n' + file_list_payloadBytes = file_list_payload.encode('ascii') + if directory and directory != '.': + os.chdir(directory) + server = TinfoilServer((host_ip, hostPort), TinfoilHTTPHandler) + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((target_ip, 2000)) + sock.sendall(struct.pack('!L', len(file_list_payloadBytes)) + file_list_payloadBytes) + while len(sock.recv(1)) < 1: + if task_canceled: + server.force_stop() + sys.exit() + time.sleep(0.1) + sock.close() + except Exception as e: + if is_logging: + logging.error(e, exc_info=True) + server.force_stop() + throw_error(1) + sys.exit() + complete_install() + server.force_stop() + sys.exit() +class TinfoilHTTPHandler(SimpleHTTPRequestHandler): def send_head(self): for s in range(len(netrlist)): if netrlist[s][0] == str(self.path)[1:]: @@ -1004,12 +1309,10 @@ class RangeHTTPRequestHandler(SimpleHTTPRequestHandler): self.send_response(200) self.send_header('Content-type', ctype) self.send_header('Accept-Ranges', 'bytes') - self.send_header('Content-Range', - 'bytes %s-%s/%s' % (start, end, size)) + self.send_header('Content-Range','bytes %s-%s/%s' % (start, end, size)) self.send_header('Content-Length', str(cont_length)) self.send_header('Last-Modified', self.date_time_string(fs.st_mtime)) self.end_headers() - return f def copyfile(self, infile, outfile): @@ -1022,9 +1325,7 @@ class RangeHTTPRequestHandler(SimpleHTTPRequestHandler): infile.seek(start) bufsize = 64 * 1024 # 64KB while True: - if is_exiting: - pid = os.getpid() - os.kill(pid, signal.SIGTERM) + if task_canceled: sys.exit() buf = infile.read(bufsize) if not buf: break @@ -1040,9 +1341,8 @@ class RangeHTTPRequestHandler(SimpleHTTPRequestHandler): except: pass except BrokenPipeError: - pass - -class MyServer(TCPServer): + pass +class TinfoilServer(TCPServer): stopped = False def server_bind(self): import socket @@ -1050,81 +1350,51 @@ class MyServer(TCPServer): self.socket.bind(self.server_address) def serve_forever(self): while not self.stopped: - if is_exiting: - pid = os.getpid() - os.kill(pid, signal.SIGTERM) - self.handle_request() + if task_canceled: sys.exit() + self.handle_request() + sys.exit() def force_stop(self): self.server_close() self.stopped = True sys.exit() -def init_tinfoil_net_install(): - reset_netrlist() - accepted_extension = ('.nsp') - hostPort = random.randint(26490,26999) - target_ip = switch_ip - hostIp = host_ip - target_path = str(selected_dir).strip() - baseUrl = hostIp + ':' + str(hostPort) + '/' - directory = target_path - file_list_payload = '' - for file in [file for file in next(os.walk(target_path))[2] if file.endswith(accepted_extension)]: - for y in selected_files: - if str(file).find(os.path.basename(y)) != -1: - n = random.randint(1,10000000) - fake_file = str(n) + ".nsp" - append_netrlist(fake_file, str(y)) - file_list_payload += baseUrl + fake_file + '\n' - file_list_payloadBytes = file_list_payload.encode('ascii') - if directory and directory != '.': - os.chdir(directory) - server = MyServer((host_ip, hostPort), RangeHTTPRequestHandler) - thread = threading.Thread(target=server.serve_forever) - thread.start() - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((target_ip, 2000)) - sock.sendall(struct.pack('!L', len(file_list_payloadBytes)) + file_list_payloadBytes) - while len(sock.recv(1)) < 1: - if is_exiting: - pid = os.getpid() - os.kill(pid, signal.SIGTERM) - sock.close() - except Exception as e: - if is_logging: - logging.error(e, exc_info=True) - server.force_stop() - throw_error(1) - sys.exit(1) - complete_install() - server.force_stop() - try: - server.shutdown() - except: - pass - sys.exit() # Tinfoil USB class Tinfoil: - @staticmethod - def send_response_header(out_ep, cmd_id, data_size): - out_ep.write(b'TUC0') - out_ep.write(struct.pack('= end_off: read_size = end_off - curr_off try: @@ -1142,7 +1410,7 @@ class Tinfoil: except: pass buf = f.read(read_size) - out_ep.write(data=buf, timeout=0) + global_out.write(data=buf, timeout=0) curr_off += read_size try: set_progress(int(curr_off), int(end_off)) @@ -1153,76 +1421,45 @@ class Tinfoil: set_start_time() except: pass - @staticmethod - def poll_commands(nsp_dir, in_ep, out_ep): + + def poll_commands(self): while True: - if is_exiting: - pid = os.getpid() - os.kill(pid, signal.SIGTERM) - cmd_header = bytes(in_ep.read(0x20, timeout=0)) + if task_canceled: sys.exit() + cmd_header = bytes(global_in.read(0x20, timeout=0)) magic = cmd_header[:4] if magic != b'TUC0': continue cmd_type = struct.unpack(' 13: - l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name[:13] + "...\"") + if len(cur_nsp_name) > 13: + if is_goldleaf: + l_status.setText("\"" + cur_nsp_name[:13] + "...\"") else: - l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name + "\"") + l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name[:13] + "...\"") else: - if len(cur_nca_name) > 13: - l_nsp.setText(Language.CurrentDict[29] + ": \"..." + cur_nca_name[-13:] + "\"") + if is_goldleaf: + l_status.setText("\"" + cur_nsp_name + "\"") else: - l_nsp.setText(Language.CurrentDict[29] + ": \"" + cur_nca_name + "\"") - + l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name + "\"") + @staticmethod def set_switch_text(): - dev = usb.core.find(idVendor=0x057E, idProduct=0x3000) - if dev is None: - l_switch.setText(Language.CurrentDict[10]+"!") - btn_header.setEnabled(False) - l_switch.setStyleSheet(RED) - else: - l_switch.setText(Language.CurrentDict[11]+"!") - l_switch.setStyleSheet(GREEN) - if list_nsp.count() > 0: - btn_header.setEnabled(True) + try: + if connect_switch(): + set_usb_success(True) + l_switch.setText(Language.CurrentDict[11]+"!") + l_switch.setStyleSheet(GREEN) + if not is_goldleaf: + if list_nsp.count() > 0: + btn_header.setEnabled(True) + else: + btn_header.setEnabled(False) + else: + btn_header.setEnabled(True) else: + l_switch.setText(Language.CurrentDict[10]+"!") btn_header.setEnabled(False) - + l_switch.setStyleSheet(RED) + try: + detach_switch() + except: + pass + except Exception as e: + if is_logging: + logging.error(e, exc_info=True) + set_usb_success(False) + UI.check_usb_success() + pass + + @staticmethod + def init_language(): + l_nsp.setText("") + if not is_goldleaf: + if list_nsp.count() > 0: + l_status.setText(str(total_nsp) + " " + Language.CurrentDict[14]) + else: + l_status.setText(Language.CurrentDict[9]) + l_switch.setText(Language.CurrentDict[10]+"!") + l_ip.setText(Language.CurrentDict[2]+":") + dark_check.setText(Language.CurrentDict[20]) + net_radio.setText(Language.CurrentDict[24]) + btn_nsp.setText(Language.CurrentDict[13]) + btn_header.setText(Language.CurrentDict[1]) + l_rate.setText(Language.CurrentDict[4]) + combo.clear() + combo.SelectedIndex = 0 + combo.addItem(Language.CurrentDict[5]) + combo.addItem(Language.CurrentDict[6]) + l_host.setText(Language.CurrentDict[3]+":") + lang_menu.setTitle(Language.CurrentDict[22]) + window.setWindowTitle(Language.CurrentDict[0]) + about.setWindowTitle(Language.CurrentDict[30]) + l_thanks.setText(Language.CurrentDict[31]) + l_donate.setText(Language.CurrentDict[32]) + about_menu.setText(Language.CurrentDict[30]) + + + @staticmethod + def lang_menu_cmd(): + new_lang = None + ai = 0 + for action in lang_menu.actions(): + if action.isChecked(): + if ai != language: + set_language(ai) + UI.init_language() + ai+=1 + + @staticmethod + def about_menu_cmd(): + try: + random.shuffle(thanks) + credit_list.clear() + for a in thanks: + credit_list.addItem(a) + about.show() + about.setFixedSize(about.size().width(),about.size().height()) + except Exception as e: + print(str(e)) + + @staticmethod + def check_usb_success(): + try: + connect_switch() + set_usb_success(True) + except Exception as e: + if is_logging: + logging.error(e, exc_info=True) + set_usb_success(False) + pass + if not usb_success: + UI.net_radio_cmd() + net_radio.setChecked(True) + usb_radio.setVisible(False) + l_rate.setVisible(False) + combo.setVisible(False) + gold_radio.setVisible(False) + l_switch.setText(Language.CurrentDict[12]) + l_switch.setStyleSheet(BLUE) +# Main +try: + # Load Images + aboutpixmap.loadFromData(base64.b64decode(ABOUT_DATA)) + goldpixmap.loadFromData(base64.b64decode(GOLD_DATA)) + iconpixmap.loadFromData(base64.b64decode(ICON_DATA)) + inlaypixmap.loadFromData(base64.b64decode(DONUT_DATA)) + dinlaypixmap.loadFromData(base64.b64decode(DARK_DONUT_DATA)) + #Init Widgets l_host = QtWidgets.QLabel(Language.CurrentDict[3]+":") txt_ip2 = QtWidgets.QLineEdit("0.0.0.0") - try: - fill = [(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1] - txt_ip2.setText(str(fill)) - except: - pass l_nsp = QtWidgets.QLabel("") l_ip = QtWidgets.QLabel(Language.CurrentDict[2]+":") l_port = QtWidgets.QLabel("Port:") @@ -1420,12 +1786,32 @@ try: btn_header = QtWidgets.QPushButton(Language.CurrentDict[1]) l_rate = QtWidgets.QLabel(Language.CurrentDict[4]) l_github = QtWidgets.QLabel("v" + VERSION) - l_status = QtWidgets.QLabel(Language.CurrentDict[9]+".") + l_status = QtWidgets.QLabel(Language.CurrentDict[9]) l_switch = QtWidgets.QLabel(Language.CurrentDict[10]+"!") list_nsp = QtWidgets.QListWidget() combo = QComboBox() + h_box = QtWidgets.QHBoxLayout() + h2_box = QtWidgets.QHBoxLayout() + h3_box = QtWidgets.QHBoxLayout() + h_group = QtWidgets.QButtonGroup() + v_box = QtWidgets.QVBoxLayout() + img_label = QLabel() + progressbar = QProgressBar() + gold_img_label = QLabel() + about = QMainWindow() + about_v_box = QtWidgets.QVBoxLayout() + credit_list = QtWidgets.QListWidget() + monero_list = QtWidgets.QListWidget() + about_img_label = QLabel() + l_thanks = QtWidgets.QLabel(Language.CurrentDict[31]) + l_donate = QtWidgets.QLabel(Language.CurrentDict[32]) #Set Widgets + try: + fill = [(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1] + txt_ip2.setText(str(fill)) + except: + pass try: txt_ip.setText(switch_ip) except: @@ -1435,74 +1821,56 @@ try: txt_ip2.setEnabled(False) txt_port = QtWidgets.QLineEdit("2000") txt_port.setEnabled(False) - h_box = QtWidgets.QHBoxLayout() - h2_box = QtWidgets.QHBoxLayout() - h3_box = QtWidgets.QHBoxLayout() h3_box.addWidget(dark_check) h3_box.addStretch() h3_box.addWidget(l_github) - h_group = QtWidgets.QButtonGroup() combo.addItem(Language.CurrentDict[6]) combo.addItem(Language.CurrentDict[5]) combo.setCurrentIndex(1) tin_radio.setChecked(True) - tin_radio.toggled.connect(tin_radio_cmd) + tin_radio.toggled.connect(UI.tin_radio_cmd) gold_radio.setChecked(False) - gold_radio.toggled.connect(gold_radio_cmd) + gold_radio.toggled.connect(UI.gold_radio_cmd) h_group.addButton(tin_radio) h_group.addButton(gold_radio) h2_box.addWidget(tin_radio) h2_box.addWidget(gold_radio) - split_check.stateChanged.connect(split_cmd) - dark_check.stateChanged.connect(dark_mode_cmd) + dark_check.stateChanged.connect(UI.dark_mode_cmd) usb_radio.setChecked(True) - usb_radio.toggled.connect(usb_radio_cmd) + usb_radio.toggled.connect(UI.usb_radio_cmd) h_box.addWidget(usb_radio) - net_radio.toggled.connect(net_radio_cmd) + net_radio.toggled.connect(UI.net_radio_cmd) h_box.addWidget(net_radio) btn_header.setEnabled(False) - progressbar = QProgressBar() progressbar.setAlignment(Qt.AlignVCenter) progressbar.setMaximum(100) - v_box = QtWidgets.QVBoxLayout() - img_label = QLabel() img_label.setAlignment(Qt.AlignCenter) + gold_img_label.setAlignment(Qt.AlignCenter) + gold_img_label.setPixmap(goldpixmap) + + # About Window + about_v_box.setContentsMargins(0,0,0,0) + about_img_label.setAlignment(Qt.AlignCenter) + l_thanks.setAlignment(Qt.AlignCenter) + l_donate.setAlignment(Qt.AlignCenter) + about_img_label.setPixmap(aboutpixmap) + about_v_box.addWidget(about_img_label) + about_v_box.addWidget(l_thanks) + about_v_box.addWidget(credit_list) + about_v_box.addWidget(l_donate) + about_v_box.addWidget(monero_list) + about.setCentralWidget(QWidget(about)) + about.centralWidget().setLayout(about_v_box) + about.setWindowTitle(Language.CurrentDict[30]) + about.setWindowIcon(QIcon(iconpixmap)) + for a in thanks: + credit_list.addItem(a) + monero_list.addItem("Monero(XMR)") + monero_list.addItem(MONERO_ADDRESS) - # Language Init - def init_language(): - l_nsp.setText("") - l_status.setText(Language.CurrentDict[9]+".") - l_switch.setText(Language.CurrentDict[10]+"!") - l_ip.setText(Language.CurrentDict[2]+":") - dark_check.setText(Language.CurrentDict[20]) - net_radio.setText(Language.CurrentDict[24]) - btn_nsp.setText(Language.CurrentDict[13]) - btn_header.setText(Language.CurrentDict[1]) - l_rate.setText(Language.CurrentDict[4]) - combo.clear() - combo.SelectedIndex = 0 - combo.addItem(Language.CurrentDict[5]) - combo.addItem(Language.CurrentDict[6]) - l_host.setText(Language.CurrentDict[3]+":") - lang_menu.setTitle(Language.CurrentDict[22]) - #git_menu.setTitle(Language.CurrentDict[23]) - window.setWindowTitle(Language.CurrentDict[0]) - # Menu Bar - def lang_menu_cmd(): - new_lang = None - ai = 0 - for action in lang_menu.actions(): - if action.isChecked(): - if ai != language: - set_language(ai) - init_language() - ai+=1 - - lang_menu = window.menuBar().addMenu(Language.CurrentDict[22]) - #opt_menu = window.menuBar().addMenu(Language.CurrentDict[21]) - #git_menu = window.menuBar().addMenu(Language.CurrentDict[23]) + about_menu = window.menuBar().addAction(Language.CurrentDict[30]) lang_group = QActionGroup(lang_menu) lang_group.setExclusive(True) lang_group.addAction(QAction('English',lang_group,checkable=True)) @@ -1516,34 +1884,31 @@ try: lang_group.addAction(QAction('Deutsch',lang_group,checkable=True)) lang_group.addAction(QAction('Bahasa Indonesia',lang_group,checkable=True)) lang_menu.addActions(lang_group.actions()) - lang_group.triggered.connect(lang_menu_cmd) - #opt_menu.triggered.connect(opt_menu_cmd) - #git_menu.triggered.connect(git_menu_cmd) + lang_group.triggered.connect(UI.lang_menu_cmd) + about_menu.triggered.connect(UI.about_menu_cmd) - # Set Language + # "And for gosh sake watch your language!" -Tony, May 1st 2015 aix = 0 for action in lang_menu.actions(): if aix == language: action.setChecked(True) aix+=1 - - init_language() + UI.init_language() # Occupy VBOX v_box.addLayout(h2_box) v_box.addWidget(img_label) v_box.addStretch() + v_box.addWidget(gold_img_label) + v_box.addStretch() v_box.addLayout(h_box) v_box.addWidget(l_ip) v_box.addWidget(txt_ip) v_box.addWidget(l_host) v_box.addWidget(txt_ip2) - #v_box.addWidget(l_port) - #v_box.addWidget(txt_port) v_box.addWidget(l_rate) v_box.addWidget(combo) - #v_box.addWidget(split_check) v_box.addWidget(btn_nsp) v_box.addWidget(btn_header) v_box.addWidget(l_nsp) @@ -1555,65 +1920,117 @@ try: window.setCentralWidget(QWidget(window)) window.centralWidget().setLayout(v_box) window.setWindowTitle(Language.CurrentDict[0]) - btn_nsp.clicked.connect(nsp_file_dialog) - btn_header.clicked.connect(send_header_cmd) + btn_nsp.clicked.connect(UI.nsp_file_dialog) + btn_header.clicked.connect(UI.send_header_cmd) window.setWindowIcon(QIcon(iconpixmap)) + gold_img_label.setVisible(False) + window.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) window.show() # Revert to network mode - if not usb_success: - net_radio_cmd() - net_radio.setChecked(True) - usb_radio.setVisible(False) - l_rate.setVisible(False) - combo.setVisible(False) - gold_radio.setVisible(False) - l_switch.setText(Language.CurrentDict[12]) - l_switch.setStyleSheet(BLUE) - if dark_mode == 0: + UI.check_usb_success() + + # Checkbox for Dark Mode + if dark_mode == 1: try: - set_dark_mode(0) + set_dark_mode(1) dark_check.setChecked(True) except: - set_dark_mode(1) + set_dark_mode(0) dark_check.setChecked(False) pass else: - set_dark_mode(1) + set_dark_mode(0) dark_check.setChecked(False) # Main loop - while True: + QApplication.processEvents() + # QMessage Response + if needresponse: + if ignore_warning_prompt == 0: + print("To ignore future prompts, change \'ignore_warning_prompt\' to 1 in fluffy.conf.") + if qrespnum == 0: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to read a file that isn't an NSP.\nLet Goldleaf read this file?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 1: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to write a file.\nConfirm file write?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 2: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to create a file.\nConfirm creation?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 3: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to create a directory.\nConfirm creation?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 4: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to delete a file.\nConfirm deletion?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 5: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to delete a directory.\nConfirm deletion?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + elif qrespnum == 6: + re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to rename a file or directory.\nConfirm rename?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if re == QMessageBox.No: + set_response_qmessage(False) + elif re == QMessageBox.Yes: + set_response_qmessage(True) + else: + set_response_qmessage(True) + while haveresponse: + time.sleep(1) + + # Check If Any Errors if last_error != "NA": - msg_box = QMessageBox.critical(window, 'Error', last_error, QMessageBox.Ok) + if not task_canceled: + msg_box = QMessageBox.critical(window, 'Error', last_error, QMessageBox.Ok) reset_last_error() - reset_install() + cancel_task() + + # Check Log Size if is_logging: - if os.path.isfile(initial_dir + '/fluffy.log'): - if os.path.getsize(initial_dir + '/fluffy.log') > 250000: - logging.debug("Fluffy Log: Logging size reached, turning off logging.") - turn_off_logging() - if os.path.isfile('fluffy.log'): - if os.path.getsize('fluffy.log') > 250000: - logging.debug("Fluffy Log: Logging size reached, turning off logging.") + if os.path.isfile(initial_dir + 'fluffy.log'): + if os.path.getsize(initial_dir + 'fluffy.log') > 250000: + logging.debug("Error: Log size reached, turning off logging.") turn_off_logging() - - QApplication.processEvents() - + + # Fix Dark Mode CheckBox + if dark_mode == 0 and dark_check.isChecked(): + dark_check.setChecked(False) + + # Save config and close if not window.isVisible(): try: switch_ip = txt_ip.text() except: pass - close_program() - pid = os.getpid() - os.kill(pid, signal.SIGTERM) - - if is_exiting: - pid = os.getpid() - os.kill(pid, signal.SIGTERM) + save_config() + cancel_task() + sys.exit() + + # Switch Indicator + if not is_installing and not is_network and usb_success and not sent_header: + UI.set_switch_text() + + # Tinfoil Network Mode if not sent_header and not is_installing and is_network: l_switch.setText(Language.CurrentDict[12]) l_switch.setStyleSheet(BLUE) @@ -1621,18 +2038,15 @@ try: btn_header.setEnabled(True) else: btn_header.setEnabled(False) - - if not is_installing and not is_network and usb_success and not sent_header: - set_switch_text() - - # Tinfoil Network Mode + + # Network Header Sent if sent_header and is_network: try: if is_done: - set_done_text() + UI.set_done_text() else: if is_installing: - set_progress_text() + UI.set_progress_text() else: l_status.setText(Language.CurrentDict[25]) l_switch.setText(Language.CurrentDict[15]) @@ -1640,28 +2054,30 @@ try: except: pass - if sent_header and not is_installing and not is_done: + # Cancel Button + if sent_header and not is_done: btn_header.setEnabled(True) btn_header.setText(Language.CurrentDict[16]) - if sent_header and is_installing and not is_done: - btn_header.setEnabled(False) + + # Installation in progress disable cancel + #if sent_header and is_installing and not is_done: + #btn_header.setEnabled(False) # Goldleaf & Tinfoil USB Mode if sent_header and not is_network: try: if is_done: - set_done_text() + UI.set_done_text() else: if is_installing: - set_progress_text() + UI.set_progress_text() else: - set_loading_text() + UI.set_loading_text() except: pass except Exception as e: if is_logging: logging.error(e, exc_info=True) - close_program() - pid = os.getpid() - os.kill(pid, signal.SIGTERM) + save_config() + sys.exit() diff --git a/misc/inlayv4dark.png b/misc/inlayv4dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a67df7c63db97089cfeba35a2b7e8cb51d51986c GIT binary patch literal 26081 zcmXteV{~Or({^mzwr$(ClZiF4ZQFJ-u{E)6+qOO5xu17^KlWK??cJ-otGc>yT^*^U zAPEnH0|NvE1TQTmrUC>6JPY`r1_c55ZI1&j00Kgd_fpexQ89KWa&Y=>Ze?pmy7G$`io z4*WUi-^c9d7o=f_j<3_bhIdV1-IjBFFebfEk3cW|(d+4F%gt-*_s7rTW6v=mZB#yH@z1qm z&p%3k#{1R&-haHj!)u|Osy+wOX#=OHrwWnkC3z6Nu!6rP@UT=4UH$yaO#fByKI)GW z!fGAlUt+GjMm#m)<->k^RDbB^_T1{eJAd!ceEW)H^o9@R`_HX@Urzb2-0p1O@aLD~ zV&gsQ^&&nWBJXPAaZy`%DOUOL{=+o*{M>%GHx3cs=ZxpcXL^@xld7pR+vK>~@bqWY zrDGmN&D~|L&DeR>d00F2%1(N= QZF4kbV=< zpL~7%#QnE=YslbIXJlVylBNi%rHKZm6~ ;7LZtQTu%vqr~3-zA_L zc_^G!G2Epx&N@ro+@sh09BFT!xL$?G }V->SPNDkn8{ePi+6H 6Yb`RHpWhnEVF76i<1P_B+vh910S1#MO z{YDM-Ki!^JYrl6o{CC+u-ROH_7aV_@Y`U(04pAt0s4uYbqu0;ct3ZKxDlH1NXOweQ zXGP-ArT>U&XfNg1U0~r)jhb}4Xx{p{-59=H7k2*J>=_nxy-~jW5#)KTaP`0368wA# zuf4usu=Bz#p)M(7GBx?)+JoC}dU96ixxf0gGr65+le_s9qH{tMIikJgpc=n2!lOo2 zi#*eGVUQGsuW+_f7tcF!UhE;$UC&i_?$Dp0z}5DwCc*I4(LDIR?6u}HFzOt>tBb%X zpV>01!YE^)K<#a}qLsAV*L1?Mcwl;Swx~+jR*k$lo=mwXO+fJz20zIY^Vf!^@Nrba zcU8_YMo2T}XT;A^V=Fq%TsDV!^F={(qL62);>GPytNUbHm9wf~&kgZ$>b!V!`m)3` z8XZGn++?$9BX`)nXIg)2CRqTyJ%m}J@ZZ5`7WLqBeYgAylWm)$x+(u{!A`?6zKuQQ z(ZCi^$F<{e_$Zj>(%FcwJ@v}Oj*&I*257)!vuKC5BA4q+-!p&udgp1UvwUnnR{!_8 zo0~RTvFjlIF{t1Q8?Xk5{Nyc!=;f5!CRg{h79G9AMi}NMZfCZv3UnOR$n~b5piN#7 z-co}bfjWe(aKmR&R7;FOV!zADLo91H>$XKu0)EtpNz( d?%AF9l0DQ zK|2Cbi>#=PYJB!WFmM8To=L~p?cFa|-42%6^dwW>4k!8mP4HPPZ2NkQg|k1*M7eG^ zLv#$|zrANnMZ|Z(JqQBKe}&=s(nAC)r1Gx9)F!G$u5vf9pM^%^sIInFr~QuN&oU(s z8(ffyw0Z8<$YPJ{#{P_Y$p=NYIS!S^zda0TzTrcwCRNir5;k2B>pj^?n7`L~?Sa0W z>oVcUQJ$*b%k`TO_%vwwH^Zcv+cF?jdT_|c?_flG{ upNT+cUZ1ULxY&E@^5gHLi6o{^0QY7~oG G7hbY06<8q{a>u<8Fclk%amx^d9JTehCea!Oyz`{&&^X zonFXUaVy|kNHQ7H0@(%BYF!Cj6V4uu8DYYy^%qx`ptCDiReTId47+UdS94C9 P+?^SA{%-;4v z25t?uZuCY9+txDIX~8-Y9?n>$vQ9zqy$Svt*2kjbM+dgl_(6%n{dU4Jmrp(p$%wb* z#BW0$P$9?Q_p5Puh4GAknOeSy) S;nk%#Cz+)IGksqqjs<#d$Liz~90SSNPCg_+ z;x0gUdDXSaCrrZAuvrloB)a^0aPx|dLy^!xW_f8-tzE_+9~zt`A&AC{vJpSN(#OFh&B)+rDH7KgAG4wIgY>M>v}eT|6szTjK2I_i^{*NK9?e+7Gc{ zJ^MHU{mpA*C4yH@#_tu`+Llce@gNUO#~)-t(dEMnp)Ib#+YThDAWj2Hd6an|hRWJO zasxs#$=e&}=P3=HbkW_8ABYO4N?-e^hh04(OxX`7o$~QL=CM9bkLt5O*V!S%wD27B z7Pw8p@w7K94tuzQSP*DlaZcdN<&MI 1B>X0#LCyKF`t ziMRe-8Ek3ntH64zRjC;Ed#%Voncg!W7Q>uC$zG& xle2+=mh2P>0d(>_vnb;SK?(BZ2cVAdb71N5dl#@V*}cO-NP&nQJZGu{3{x{>1O0 zjoxG}`O`6)WY8anTPjYm-^2fhL25WjaZ87WWn?NWKcoKvOkl_qcP8mobke{xtyI*g z51}L=g$bs6Qie>SDui!qFfm|HTgk~g6NV(TrnZ xubO1v4e6KX7 zKjF@! tl>^3|a*`ZHfyg4^s4~IS}a-kpbWjr{OdWIk# z<4ur4^osO#>k8itMFkJwE`A8O`|%2ua5LhJugaJAED4!Du&B0 ztwvziJ)ltr=K~R(6)lA1Bp$qxuS0nv3N2}f^2E-h=|?1vT~(Le&RyphaftQ OM)=a)XbqSb$Q3rkiO5GpMd)h4jeX?cd{r?a|BU9g9q~0q z4DJ&O&bkid$l#}Dh6POUyA+f14Dk;N2?itgp6DuVDv(p=P~cC2mJgZe6>uPOQ6 o?8r3>GbLfd1B)oG4L8As7u}D?>qKb^kDYqAT{4Kw$MWb$G8h#JVu~A(o z3~&_H+OB?(YR%yfP>fTghbEHoBys1ALOFCDhz_UCA Hrh;C|GNr5EnwwX2a3?^eOYiU!tWiwIgD*uA!R3V>dfY@JPN2`ix;@ZeXVn~kj zldQ 7vTWRw*FqD42Z&U z#@NA{#NX_ZN%S<3asubtP3!jI9fFJ}W>+Wz64KefHXNCT<+~^a5P+t~58~lm68>cX zanRy{V$n5j*P6E34}DnuO2kqSR!wZtc7>!G`J1B57Dz6>rA!h$ LAE$6@;iZV=RCk>8t&TV~I1KY375YNUl;k-yR1X(1f z=Y+2MX(4t;&D&+%T!4?IhoGQl8)-y4psJ!M+9H;1Z10a$Mw~6XS{SPUe>Rf^1=H9? zRLc^JmQJF)kV&qJ0eOyIc)JSHpTWB6HHNi^quXUP3x0;7BJ5*|ltHaUyMLDHpWsvY zM1v?!RJkES7vg%&y<&vQ&7%!7)9DGM2z>U4dWxR;Cr |IaFls b2tJD=J2>b;~Sw`fn6?}q}hsU0T86M+qf*k#8TVQ z9M&aI!0DI8^w)^CK~!wxaz wP~@DAgo{XRyWzb@+8 zhOob|?r)?ltuH~Loo(5~X MKDb-wViw#uM_4=`1DP(l#Ww!uAb!LJ)d zV7B7!ct`*4W^FEsicl6$jThrATIB uSuj79>1OI->q|$ )j=Apn7wAUD#M-p>;kn#i31| z;$_>u`&YlPm0^oEm0;3a9#ve@)1y<2{VumTv-u8+U`a>ArM}j<8kQq-$~B-63T(r! zW1)#a$B~Gmgq%$A@Rpajvfx!9c0mg?qB>o6DXiN{{B=T20jyUekNOG|7~6=(5%rxh zFRqUbDk*V*FCwbdn?Y$ wHq89(TL`qnkT?Nw2Qb*vz^JI!4sGRz zXY&z5bO`Yh>o*Y_R_tk2-7IB+)uPS_E6Vw=Gmtyw21a 7tTd6cxKqATY -g4 tD?}sW{)DuE{g`#2ef%F$_YehVGg_WXZhi4+nggeFQWL63y7!ZGk z7_DkqFVr9?!PXQA^f&uTsypr|Ha?Txi?JCSLZhX+fSR~btULei6~tQ$?^!XS%y=^o zd&GxmASKT8J|N~fS?(fiSxA7PN&=M_IJkhSa!vUW@jMasl#PuVeCi$Z%5(GFtDoSA zWkF!b3aiW(D`GHVfx~~UKnI^D?A$B|N)5w{U2iGl1cLB6(9V?bl?@H0o`3p1DYJfs zVs1k^jvSj_v&mh5?(-r23dz|DNVrlXZmm>|p<)g!KOCdKXbMRqpd&SbO3)6AL72g# zJS(KVW= 0vk3@p9xR(&Iy<=ul+2=4=;r zxT|S_4ODY@l2wdqn$16Lv3Q4N_n+%sqblTu{BN?C-;~gOnX|(~k(v7~Wi6 z?ld0zakMIWq*#_cL|wZE)drUKbHCY4Cfi=@_RNvoc!1;|MUQtnTO7Jp=O(2+xlDm$ zV;k~k&D118J|?79_4$6i!Tq}}5R>vZh@gkYSRNRS9k1fMYFT_h;P}{BW{wdZPsE9= zivsNNGP6PN=hXGK&HLB=`%h}yZg1-V`y9GmkP%Fa@V|Gp)4t;OQ413Px9_hi!7s$# zmczkA Q(eQ@aYP zcAp>IuEsxPzv6P)09aj!m8htaw5TYNl8T~)w468qkjwE+ qkVw)T9*0q`vo; zE) Z-C-pcA!df26iXO6B+AwU^-?jn33Su2as>&UQqb)0F; zpPosHfC_s|kmi-4?h@*e#YIC)bL{ #xs}|@q@||Ws8`M%MV6Kzf&)Uv zrX&teanM{@f;BehH!v({^0|1FG^P$tItp^3M6}^|C-}BqqnwrHk`x5p_s>L|@RyJg zrX?r+<>y-C24%o4cDu^oA1~O9Z?f+7 tb>k!<@R0AdPZ zA}c8d^Z;}Sl#qZY{txg5+CfU&83+g#^}h!gC@TjG@DkERT3#IT5F87UiBL;du?+}_ z2uNB?Sj}VOD%UfIOg!_oH&NT&?ZtP>*KHbD&Ka(aiYThB@uH}<)1aN;_M-AHNYv#8 z4ah}p)#t@!cg0_VfT${}a4M)Eyg@kUzA3d?-)CQSGk5*W`=7*h{$+P{R}FVp_n$X3 zReg1AY(+8CuLIA6$WvsGJ0KtTf5vsld>(gu#EMkW(rcUcU2%X1Az;?$=-THef9E_F z2$2BrU5RN(gmlz+%i+?U@{8~FQ}?$AqKZtIiq+3^)xT@2y#L1R89Q3#{epISyN!%+ z##*nAL;~rZJ>XpMV5T3^p4%@f@K1kG9Q@lhSmDSI~HG(*9sbz~`e_aGTa#xQ-J& z#Pa=2JO2M zNXY`Rs#T`eIvi1h9BKlQ-Fc|$hzX@yJm3F>0VkYBXi$@*L<$k1p|fKR5Y`eEsG}@Y z-GL8^Raz^3`-ymw3-~+asI99-nVJBxCL7oHhRZocsCDH(?Ez&92itl *z6IvtE5|KAk-qVI|2@>wWW(ji5Auo6%g%VO5J^SJ3s%k44s29bhj zBbZPHP-bGAXsQ|3;Vnv&Y>8>&9LW@{Y(slOgx~m-k?yU`Ym3q}^Wm8|B03l%*c9`N zIOm&!Z@@3MyZyBmdBn*wo<|*0NYS{$d)9mCeLMvymXI&Nz0c71r!DBni#F5%OFqz1 zTjS!MpPyw2O%AP4T&5{?+oHSDuBs^ODiT%iKYk)deU{sVjkB>^CY;@rT%Hj_DBzpb z*P*gJVt-z^m-tPSs)k4aG2o+6#lyzlVo!NL8>mO@98K$DbnSN|J)@))kva?&u^25Hz zCwxIMmYv|{DMzx8CPBWjxbi%c&0q14W5I|lDN-Is9YS+BoFphcuwA7AlYT{ge^)Pc z7How7Q_Lll&Cum}+2=Ca@Ss((YjuT%9(MRvvdU26dqbYBb~C_0H=YrRD)}VQRK*H~ zKew51OL4Wo8_uoG*o+lQT$qYx)qh`?&+`jw8z0}+|3LcZsSlLTXV(`FJ4`a~5i1-e z*ji&^7l@JZ;9*zo{kzT%4!mSF3E AL<0CQ5l9d^Gy^ws&X zTRe9gZ{`s>P~Va4=^4Q*!JY{rK^aMAWGRlO9`sS~G4Elj`~CYZBd*lY?#OyKj*WKz zpV_-Oho@*|MM)jfLRbS^`e=wiTtImP3?*IddMTsBlaee-ih|PrEwguBW!ZS_;_h%A zK!t=97>Evr(O2_b^3hmdOk}Z9lFWO!V@kT)6<4`({t^2ZQLMrcnaAT86n@*A|LNz+ z`M1?BTYjMn><#KprobdHez!c}Rnm2OS^iXOODpyo3N1&NSrlZ2^KygwcNVp*)1M>< z;Fn9E)7?~0y${+wRAqhdo#bKC*`WI3;_uQvLr_d$bpzDSIKMsUy%}emY=W`ZqRadp zReG?0m>!oTHg&>#HOl&bH^m>mK<=P1qdrX3R_*V?Ts(;|IA>f=YSVeiRIe(1q2B&h z)PA>7MoZ?sqnuJUbR($Ooy#DgzCCfr${UV}Zt8(-A=+w}*rfs+;4HU<>t|)W09mvv zE%j8?+C79l M?wD18iCQKS7o`y^j&jE6`8qs#XR0Yps%Pd&qlN;yW6- zeIpgM&od=RqhKqi8l`9zdUZCAzo}-St?Yq&5W4EsqB(%m*)KPe^XQ@xL-De{Y~r~r z@GBQ@k_Uo*dk#ShfwDT^0SgUi+iOe&dDYpRgm4Lh*wPZO1HF;^tp8`8bBDxLJ?9|l z&F?TfZv$~-3nWaQoxg2@%!|SOwOKvjCEfO!{LLfq{&e ~e1ZH~Kxe_aVzjWXdJJVSTvs z>aO#b+g$#*4?(+IoPF;GVC9O4kC5pe@=U~0FDb4p<#dl@&VIxgoV5c2J 1|Aauk4a|Il+ -A3`=6ju9nXugRmk rXQ9KO3Ix2Jkt#ZSmXHxE}Ax5^ExiW?DCFR=&uNw21JmYs`#J{ zgx(ek(e5=N>mShtBd@Q}S?RGi)zlN8O|>)DHuLZPL~#*DfEe3SPHije^J=kq+WYpy zwjWK |QA$d?`8 in2;n+XrnbF_hUw@>GV^Y z?gghg!4PE4Ny*$MX9|eyJt3z7n;BcUT0oj0iX#Ujf{aIg8-hk^hxYdS$tW|sya`Ht z+EE0>jPb*<+kJ;?fieb&U1%Pp4y<=-boP7%(YVeRs>so}`5=C5@Qx=WFnnCv-J@1{ zr-okCHd)!oB5Q(3L4b0umZ)gVoTVfjLK+8} NG$dO pY2b~Cb~gk6RUMEY zG}vR881N4}2Io^nt-hckZxOpan~VqUos1Fb9M~Ihj@7ulEtp?9Oam_lr@%og ~*If4T7KTZi4Al2p#T3 z3p#sS@Ee){F6aAutt}6pN*nj7as;jvEVU*TKa_TY3_H$$O#zT5j)zTv3{p)wxwHn@ zA~j+^=&s6uNw7Od^$4ODj3HZO3;4TKY&2!^eYd?R>G8)H=UL;&H2OE6m(f!;4#&;d zRpLh`UJ6vlxErr9)!E;a=(pfj+;+!EHckm@UGYDv;pcqq>FTF?a*LhtZy z92@8cLnF^OS*sV@UJ$Wfe^)GhoQNK_jl|MGy8~&?8nc7lWom8RNxELE=FugKq8}`R zqgXHq;6>vqin(5oHZ5V?LS9&CjT|?$-xmSWFZJpaYY;%X >w@pyJM$3r z==G<0a&YslZ k zqkzEoBZ-1Ps+eqQk9*WST>rJuc*cuy*oW#*f=!s=hfNJ;lbyZpagqt}o!%4f#lh3x zE`JDq)4CSpN8q*q%!LRz;D&Vec(KE=rgZi$!v`aAT3c_8zx?vN?xFL+YiXM9@0LLw zi}##+VWrck@0iW=fXzEdhP-9zJ?QMKAzRzkwvJJgPA6P#w0n&YBgWli?Y1MvIaD^; z?u8PQ@{&iby#07rs18z4y)$@1SVRi$4dMho!9+b1Bs)lwWgKl^-43oLormzcmBxIV zC3 FL)@ z>pXjNckNR*(cuFLaes=(-nCH+n#cAddK3b86oL5Vr5_Ba)bot?GNBsh{xKHuT~5%+ zZ4y#- m5zh`m{6*JeM}zYu%SN`@o|Yoq iP@ zGo|N!zmUv1SZ~dBt+bz7a*1-!2q{;eFeekc_Y3nB4z_a_lWG;!wVQe}%g|~0DxC@K zYXhPM4$rHH^jV1~GkIlV1`3A?qORdo&q|iywoW|+P7YGd_$YK>zP !GCZ@Aa;OHYqfBGr5RetAz=bsZ(H{ag zDx9lxvSgX*zw0Y(@+jN*^+S^%4nij6_LfUY+%2qz1$lHnv8kn9d&kN#!JB+7>2b zGVfM<=&W-=y@DrA&t`P?g%;-+Npwy3IY4??(YWN1K)=>ETC<;h-IkhozRDcWQPfVi zjx*KONdu8d*q^}$H^8Ej$&&FJXC0lgpkPfDp^2Clsi&s+miXk8pXTGUN4k5Bb_BjZMxfjfDveuw;N;pFg0LnQmq= z6V7YTRz ; znZ|jUWy9@&pL{K=Z$Ai0k;7ztc{wV-7j+<7WhbPZ3QhTGBqn FB@7=rkLfzhkjeky=NgQ z$@?P|P-Z53dUU=CsHfi1_a|lkREoUS55|C(UK?Wr1r7Z04xFc-eCcwZsK;KXYC|qn ziY$$P$;sm8gddD(ZMEz62?A26GUkvo>(;W|*y8dG`rM(3qy6={zAqJXMQu$)MqeNc zMb0h;j|C% HioI4c$&D!81?l2Jsx!MLfl zaYa#5fq}d^6|+diJI_?iU9(a}g<%jQ5}jWV0PiU(c>l8W6Ex08%r7c{3QQ8s8>} MU7$%NR{6d83(~c3Sh)4D{A%2Iq~nLRmgT$S0^t@J+i$= zZ)i^((*}p{MLR96jOqhHBc@1oNK*yUBi$bcMAr|M(R(^QXZ^qVa)z3z p%2|D~_aPV17} z>5vhzt0<%{iCm$m3{^oxoF9Lo6k( MVMywRK6GX^J;_(hpOa+jlx$accFV|91AL_QLs4zG+fEZmm z{QmKtixR|zjM8hX`y-jbs64)d(rr6H6lR4j9`&<<(S(RHy;OM(ds(N|Oy#?TMe3?+ zW6W!*YK!Z0i72T{Hcx%EsHavMC8T }|P2pIP3*6FCj$KtjYyn%2(P%omK-u?C| zgp%cKL5h*{;~_3kKe7A^^Bj9@jh)2`7VyszPv=BLH^-7#g-)YWD_$YJNwd8p0)`v) z9(m8kj3dtv4bbtR3Ddwzr19PTCZ~D~1|*IbYzCuIdXZAR-!FL&5IshgL!j;pX8xk6 zw5@s(j3u!olEf+VWEVbeSzN>_t3wyg3}jg2yKvM%+k{4`2GaUuF^1~p#9im7V);c% z=x~jljZqs)?L_oy&!5^oR YNZTTZ8|G+c4{kGMVI$4` zK2NnY+!y3iWI(OiN8HJf?y=F&T-<~_Gn7N+0fMR0t~bxnj=7YR&9I>sfr6cSSEUW@ z`y-x9y{;Vw{#jK01MT5_NHETq@VBjyB19odWfY3brxx!g0Py7%oSWIfIYLDFdKHT$ za*4k1eW9vCyOZR1jVf|bmQqA&r^3)}ev+8nVo7;b@`~@Q<6EEK0qFPQ%_xLgYAHH8 zn>d?~%+I4?NoCd &3~37%$dDV%wdp>n_8z zR(eeW|8nwFP|}jh9Kimut!9*s_pcfFIiFr{JvQAwT;}^_fBHYG@@y)~%NA53Erkv) zL4rVK?)uP#6f_n_$}@Ul%g%PraWq9m;)jP6AyroXT3*3K?mm)ft~`$qn||$kZ;D7j z62Enw>Nr^k0EF$h{l{-U=k3p(E2OrJ7rFt3+Fj4k;b0u%uGgq>koA uy%Ry{v;0zxj^cHcdI(#0fUknI|0ng!txwj~`YGzRi-y8V`aRa!?J* u zh#ljmAjhSS T<3Cu@@DShbbY+k!*HP%EwOxX$CMv63WH6=}LfvHN$ZkbyUle(Fen7L%Uu@Le%w z-$xdRJG$b^vCFNeB)W^OR2RXn&H4AWz!~Fr-JDgfp@Xm|Q)*lv!Z*Adb8;_3<|C{1 z6Q{O#k}c&7PiYw>5Pzne-T7o_DK;dSV)a#(<@S(?Fba}Xb-*-1k*Kzas@=xa) 4Hh)WQh@7?6(c3 DezSY1Jlu>?mg*Dnz1`sUKLw&Ay?!{6G%rFofq&;fw|Q