GUI: Implement download GUI class

Unifies all download UIs
This commit is contained in:
Mykola Grymalyuk
2023-05-07 17:41:46 -06:00
parent bd70c4a24a
commit 3ef6e4a853
6 changed files with 445 additions and 39 deletions
+6 -1
View File
@@ -9,6 +9,7 @@ import threading
import logging import logging
import enum import enum
import hashlib import hashlib
import atexit
from pathlib import Path from pathlib import Path
from resources import utilities from resources import utilities
@@ -340,6 +341,7 @@ class DownloadObject:
response = NetworkUtilities().get(self.url, stream=True, timeout=10) response = NetworkUtilities().get(self.url, stream=True, timeout=10)
with open(self.filepath, 'wb') as file: with open(self.filepath, 'wb') as file:
atexit.register(self.stop)
for i, chunk in enumerate(response.iter_content(1024 * 1024 * 4)): for i, chunk in enumerate(response.iter_content(1024 * 1024 * 4)):
if self.should_stop: if self.should_stop:
raise Exception("Download stopped") raise Exception("Download stopped")
@@ -407,7 +409,10 @@ class DownloadObject:
if self.total_file_size == 0.0: if self.total_file_size == 0.0:
logging.error("- File size is 0, cannot calculate time remaining") logging.error("- File size is 0, cannot calculate time remaining")
return -1 return -1
return (self.total_file_size - self.downloaded_file_size) / self.get_speed() speed = self.get_speed()
if speed <= 0:
return -1
return (self.total_file_size - self.downloaded_file_size) / speed
def get_file_size(self) -> float: def get_file_size(self) -> float:
+38
View File
@@ -48,6 +48,44 @@ def human_fmt(num):
return "%.1f %s" % (num, "EB") return "%.1f %s" % (num, "EB")
def seconds_to_readable_time(seconds) -> str:
"""
Convert seconds to a readable time format
Parameters:
seconds (int | float | str): Seconds to convert
Returns:
str: Readable time format
"""
seconds = int(seconds)
time = ""
if seconds == 0:
return "Done"
if seconds < 0:
return "Indeterminate"
years, seconds = divmod(seconds, 31536000)
days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
if years > 0:
return "Over a year"
if days > 0:
if days > 31:
return "Over a month"
time += f"{days}d "
if hours > 0:
time += f"{hours}h "
if minutes > 0:
time += f"{minutes}m "
if seconds > 0:
time += f"{seconds}s"
return time
def header(lines): def header(lines):
lines = [i for i in lines if i is not None] lines = [i for i in lines if i is not None]
total_length = len(max(lines, key=len)) + 4 total_length = len(max(lines, key=len)) + 4
+80
View File
@@ -0,0 +1,80 @@
import wx
from resources import constants, network_handler, utilities
class DownloadFrame(wx.Frame):
"""
Update provided frame with download stats
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, download_obj: network_handler.DownloadObject, item_name: str, screen_location: tuple = None):
self.constants: constants.Constants = global_constants
self.title: str = title
self.parent: wx.Frame = parent
self.download_obj: network_handler.DownloadObject = download_obj
self.item_name: str = item_name
self.frame_modal = wx.Dialog(parent, title=title, size=(400, 200))
self._generate_elements(self.frame_modal)
def _generate_elements(self, frame: wx.Frame = None) -> None:
frame = self if not frame else frame
title_label = wx.StaticText(frame, label=f"Downloading: {self.item_name}", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Center(wx.HORIZONTAL)
label_amount = wx.StaticText(frame, label="0.00 B downloaded of 0.00B (0.00%)", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
label_amount.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_amount.Center(wx.HORIZONTAL)
label_speed = wx.StaticText(frame, label="Average download speed: Unknown", pos=(-1, label_amount.GetPosition()[1] + label_amount.GetSize()[1] + 5))
label_speed.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_speed.Center(wx.HORIZONTAL)
label_est_time = wx.StaticText(frame, label="Estimated time remaining: Unknown", pos=(-1, label_speed.GetPosition()[1] + label_speed.GetSize()[1] + 5))
label_est_time.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_est_time.Center(wx.HORIZONTAL)
progress_bar = wx.Gauge(frame, range=100, pos=(-1, label_est_time.GetPosition()[1] + label_est_time.GetSize()[1] + 5), size=(300, 20))
progress_bar.Center(wx.HORIZONTAL)
# Set size of frame
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
frame.ShowWindowModal()
self.download_obj.download()
while self.download_obj.is_active():
if self.download_obj.total_file_size == -1:
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded"
else:
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded of {utilities.human_fmt(self.download_obj.total_file_size)} ({self.download_obj.get_percent():.2f}%)"
label_amount.SetLabel(amount_str)
label_amount.Center(wx.HORIZONTAL)
label_speed.SetLabel(
f"Average download speed: {utilities.human_fmt(self.download_obj.get_speed())}/s"
)
label_est_time.SetLabel(
f"Estimated time remaining: {utilities.seconds_to_readable_time(self.download_obj.get_time_remaining())}"
)
progress_bar.SetValue(int(self.download_obj.get_percent()))
wx.GetApp().Yield()
if self.download_obj.download_complete is False:
wx.MessageBox(f"Download failed: \n{self.download_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
frame.Destroy()
+298
View File
@@ -0,0 +1,298 @@
import wx
import logging
import threading
from pathlib import Path
from resources.wx_gui import gui_main_menu, gui_support, gui_download
from resources import (
constants,
macos_installer_handler,
utilities,
network_handler,
integrity_verification
)
class macOSInstallerFrame(wx.Frame):
"""
Create a frame for downloading and creating macOS installers
Uses a Modal Dialog for smoother transition from other frames
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
# super(macOSInstallerFrame, self).__init__(parent, title=title, size=(350, 200))
self.constants: constants.Constants = global_constants
self.title: str = title
self.parent: wx.Frame = parent
self.available_installers = None
self.available_installers_latest = None
self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200))
self._generate_elements(self.frame_modal)
self.frame_modal.ShowWindowModal()
def _generate_elements(self, frame: wx.Frame = None) -> None:
"""
Format:
- Title: Create macOS Installer
- Button: Download macOS Installer
- Button: Use existing macOS Installer
- Button: Return to Main Menu
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label="Create macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Center(wx.HORIZONTAL)
# Button: Download macOS Installer
download_button = wx.Button(frame, label="Download macOS Installer", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(200, 30))
download_button.Bind(wx.EVT_BUTTON, self.on_download)
download_button.Center(wx.HORIZONTAL)
# Button: Use existing macOS Installer
existing_button = wx.Button(frame, label="Use existing macOS Installer", pos=(-1, download_button.GetPosition()[1] + download_button.GetSize()[1] - 5), size=(200, 30))
existing_button.Bind(wx.EVT_BUTTON, self.on_existing)
existing_button.Center(wx.HORIZONTAL)
# Button: Return to Main Menu
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, existing_button.GetPosition()[1] + existing_button.GetSize()[1] + 5), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return)
return_button.Center(wx.HORIZONTAL)
# Set size of frame
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
def _generate_catalog_frame(self) -> wx.Frame:
super(macOSInstallerFrame, self).__init__(None, title=self.title, size=(300, 200))
self.SetPosition((self.parent.GetPosition()[0], self.parent.GetPosition()[1]))
# Title: Pulling installer catalog
title_label = wx.StaticText(self, label="Pulling installer catalog", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Center(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(250, 30))
progress_bar.Center(wx.HORIZONTAL)
progress_bar.Pulse()
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
self.Show()
# Grab installer catalog
def fetch_installers():
remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=macos_installer_handler.SeedType.DeveloperSeed)
self.available_installers = remote_obj.available_apps
self.available_installers_latest = remote_obj.available_apps_latest
thread = threading.Thread(target=fetch_installers)
thread.start()
while thread.is_alive():
wx.Yield()
progress_bar.Hide()
self._display_available_installers()
def _display_available_installers(self, event: wx.Event = None, show_full: bool = False) -> None:
self.frame_modal.Destroy()
dialog = wx.Dialog(self, title="Select macOS Installer", size=(300, 200))
# Title: Select macOS Installer
title_label = wx.StaticText(dialog, label="Select macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Center(wx.HORIZONTAL)
# Subtitle: Installers currently available from Apple:
subtitle_label = wx.StaticText(dialog, label="Installers currently available from Apple:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
subtitle_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
subtitle_label.Center(wx.HORIZONTAL)
# List of installers
installers = self.available_installers_latest if show_full is False else self.available_installers
if installers:
spacer = 0
for app in installers:
logging.info(f"macOS {installers[app]['Version']} ({installers[app]['Build']}):\n - Size: {utilities.human_fmt(installers[app]['Size'])}\n - Source: {installers[app]['Source']}\n - Variant: {installers[app]['Variant']}\n - Link: {installers[app]['Link']}\n")
extra = " Beta" if installers[app]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else ""
installer_button = wx.Button(dialog, label=f"macOS {installers[app]['Version']}{extra} ({installers[app]['Build']} - {utilities.human_fmt(installers[app]['Size'])})", pos=(-1, subtitle_label.GetPosition()[1] + subtitle_label.GetSize()[1] + 5 + spacer), size=(270, 30))
installer_button.Bind(wx.EVT_BUTTON, lambda event, temp=app: self.on_download_installer(installers[temp]))
installer_button.Center(wx.HORIZONTAL)
spacer += 25
# Show all available installers
show_all_button = wx.Button(dialog, label="Show all available installers" if show_full is False else "Show only latest installers", pos=(-1, installer_button.GetPosition()[1] + installer_button.GetSize()[1]), size=(180, 30))
show_all_button.Bind(wx.EVT_BUTTON, lambda event: self._display_available_installers(event, not show_full))
show_all_button.Center(wx.HORIZONTAL)
# Return to Main Menu
return_button = wx.Button(dialog, label="Return to Main Menu", pos=(-1, show_all_button.GetPosition()[1] + show_all_button.GetSize()[1] - 7), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Center(wx.HORIZONTAL)
# Set size of frame
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
dialog.ShowWindowModal()
self.frame_modal = dialog
def on_download_installer(self, app: dict) -> None:
self.frame_modal.Close()
download_obj = network_handler.DownloadObject(app['Link'], self.constants.payload_path / "InstallAssistant.pkg")
gui_download.DownloadFrame(
self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition(),
download_obj=download_obj,
item_name=f"macOS {app['Version']} ({app['Build']})",
)
if download_obj.download_complete is False:
self.on_return_to_main_menu()
return
self._validate_installer(app['integrity'])
def _validate_installer(self, chunklist_link: str) -> None:
self.SetSize((300, 200))
for child in self.GetChildren():
child.Destroy()
# Title: Validating macOS Installer
title_label = wx.StaticText(self, label="Validating macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Center(wx.HORIZONTAL)
# Label: Validating chunk 0 of 0
chunk_label = wx.StaticText(self, label="Validating chunk 0 of 0", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
chunk_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
chunk_label.Center(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, chunk_label.GetPosition()[1] + chunk_label.GetSize()[1] + 5), size=(270, 30))
progress_bar.Center(wx.HORIZONTAL)
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
self.Show()
chunklist_stream = network_handler.NetworkUtilities().get(chunklist_link).content
if chunklist_stream:
utilities.disable_sleep_while_running()
chunk_obj = integrity_verification.ChunklistVerification(self.constants.payload_path / Path("InstallAssistant.pkg"), chunklist_stream)
if chunk_obj.chunks:
progress_bar.SetValue(chunk_obj.current_chunk)
progress_bar.SetRange(chunk_obj.total_chunks)
wx.App.Get().Yield()
chunk_obj.validate()
while chunk_obj.status == integrity_verification.ChunklistStatus.IN_PROGRESS:
progress_bar.SetValue(chunk_obj.current_chunk)
chunk_label.SetLabel(f"Validating chunk {chunk_obj.current_chunk} of {chunk_obj.total_chunks}")
chunk_label.Center(wx.HORIZONTAL)
wx.App.Get().Yield()
if chunk_obj.status == integrity_verification.ChunklistStatus.FAILURE:
wx.MessageBox("Chunklist validation failed.\n\nThis generally happens when downloading on unstable connections such as WiFi or cellular.\n\nPlease try redownloading again on a stable connection (ie. Ethernet)", "Corrupted Installer!", wx.OK | wx.ICON_ERROR)
self.on_return_to_main_menu()
return
# Extract installer
title_label.SetLabel("Extracting macOS Installer")
title_label.Center(wx.HORIZONTAL)
chunk_label.SetLabel("May take a few minutes...")
chunk_label.Center(wx.HORIZONTAL)
progress_bar.Pulse()
# Start thread to extract installer
self.result = False
def extract_installer():
self.result = macos_installer_handler.InstallerCreation().install_macOS_installer(self.constants.payload_path)
thread = threading.Thread(target=extract_installer)
thread.start()
# Show frame
self.Show()
# Wait for thread to finish
while thread.is_alive():
wx.Yield()
if self.result is False:
progress_bar.SetValue(0)
chunk_label.SetLabel("Failed to extract macOS installer")
chunk_label.Center(wx.HORIZONTAL)
wx.MessageBox("An error occurred while extracting the macOS installer. Could be due to a corrupted installer", "Error", wx.OK | wx.ICON_ERROR)
progress_bar.Hide()
chunk_label.SetLabel("Successfully extracted macOS installer")
chunk_label.Center(wx.HORIZONTAL)
# Create macOS Installer button
create_installer_button = wx.Button(self, label="Create macOS Installer", pos=(-1, progress_bar.GetPosition()[1]), size=(170, 30))
create_installer_button.Bind(wx.EVT_BUTTON, self.on_existing)
create_installer_button.Center(wx.HORIZONTAL)
# Return to main menu button
return_button = wx.Button(self, label="Return to Main Menu", pos=(-1, create_installer_button.GetPosition()[1] + create_installer_button.GetSize()[1]), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Center(wx.HORIZONTAL)
# Set size of frame
self.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
# Show frame
self.Show()
result = wx.MessageBox("Finished extracting the installer, would you like to continue and create a macOS installer?", "Create macOS Installer?", wx.YES_NO | wx.ICON_QUESTION)
if result == wx.YES:
self.on_existing()
def on_download(self, event: wx.Event) -> None:
self.frame_modal.Close()
self.parent.Hide()
self._generate_catalog_frame()
self.parent.Close()
def on_existing(self, event: wx.Event = None) -> None:
pass
def on_return(self, event: wx.Event) -> None:
self.frame_modal.Close()
def on_return_to_main_menu(self, event: wx.Event = None):
if self.frame_modal:
self.frame_modal.Hide()
main_menu_frame = gui_main_menu.MainMenu(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
if self.frame_modal:
self.frame_modal.Destroy()
self.Destroy()
+8 -1
View File
@@ -4,6 +4,7 @@ from resources.wx_gui import (
gui_sys_patch, gui_sys_patch,
gui_support, gui_support,
gui_help, gui_help,
gui_macos_installer,
) )
from resources import constants from resources import constants
@@ -93,7 +94,13 @@ class MainMenu(wx.Frame):
def on_create_macos_installer(self, event: wx.Event = None): def on_create_macos_installer(self, event: wx.Event = None):
pass gui_macos_installer.macOSInstallerFrame(
parent=self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
def on_settings(self, event: wx.Event = None): def on_settings(self, event: wx.Event = None):
pass pass
+15 -37
View File
@@ -20,6 +20,7 @@ from resources.sys_patch import (
from resources.wx_gui import ( from resources.wx_gui import (
gui_main_menu, gui_main_menu,
gui_support, gui_support,
gui_download,
) )
class SysPatchMenu(wx.Frame): class SysPatchMenu(wx.Frame):
@@ -89,46 +90,23 @@ class SysPatchMenu(wx.Frame):
# KDK is already downloaded # KDK is already downloaded
return True return True
kdk_download_obj.download() gui_download.DownloadFrame(
self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition(),
download_obj=kdk_download_obj,
item_name=f"KDK Build {self.kdk_obj.kdk_url_build}"
)
if kdk_download_obj.download_complete is False:
return False
header.SetLabel(f"Downloading KDK Build: {self.kdk_obj.kdk_url_build}") header.SetLabel(f"Validating KDK: {self.kdk_obj.kdk_url_build}")
header.Center(wx.HORIZONTAL) header.Center(wx.HORIZONTAL)
progress_bar.SetValue(0) subheader.SetLabel("Checking if checksum is valid...")
# Set below developer note subheader.Center(wx.HORIZONTAL)
progress_bar.SetPosition( wx.GetApp().Yield()
wx.Point(
subheader.GetPosition().x,
subheader.GetPosition().y + subheader.GetSize().height + 30
)
)
progress_bar.Center(wx.HORIZONTAL)
progress_bar.Show()
developer_note = wx.StaticText(frame, label="Starting shortly", pos=(-1, progress_bar.GetPosition().y - 23))
developer_note.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
developer_note.Center(wx.HORIZONTAL)
frame.SetSize(-1, progress_bar.GetPosition().y + progress_bar.GetSize().height + 40)
while kdk_download_obj.is_active():
subheader.SetLabel(f"{utilities.human_fmt(kdk_download_obj.downloaded_file_size)} downloaded of {utilities.human_fmt(kdk_download_obj.total_file_size)} ({kdk_download_obj.get_percent():.2f}%)")
subheader.Center(wx.HORIZONTAL)
developer_note.SetLabel(
f"Average download speed: {utilities.human_fmt(kdk_download_obj.get_speed())}/s"
)
developer_note.Center(wx.HORIZONTAL)
progress_bar.SetValue(int(kdk_download_obj.get_percent()))
wx.GetApp().Yield()
if kdk_download_obj.download_complete is False:
logging.info("Failed to download KDK")
logging.info(kdk_download_obj.error_msg)
# wx.MessageBox(f"KDK download failed: {kdk_download_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
msg = wx.MessageDialog(frame, f"KDK download failed: {kdk_download_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
msg.ShowModal()
return False
if self.kdk_obj.validate_kdk_checksum() is False: if self.kdk_obj.validate_kdk_checksum() is False:
logging.error("KDK checksum validation failed") logging.error("KDK checksum validation failed")