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

View File

@@ -9,6 +9,7 @@ import threading
import logging
import enum
import hashlib
import atexit
from pathlib import Path
from resources import utilities
@@ -340,6 +341,7 @@ class DownloadObject:
response = NetworkUtilities().get(self.url, stream=True, timeout=10)
with open(self.filepath, 'wb') as file:
atexit.register(self.stop)
for i, chunk in enumerate(response.iter_content(1024 * 1024 * 4)):
if self.should_stop:
raise Exception("Download stopped")
@@ -407,7 +409,10 @@ class DownloadObject:
if self.total_file_size == 0.0:
logging.error("- File size is 0, cannot calculate time remaining")
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:

View File

@@ -48,6 +48,44 @@ def human_fmt(num):
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):
lines = [i for i in lines if i is not None]
total_length = len(max(lines, key=len)) + 4

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()

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()

View File

@@ -4,6 +4,7 @@ from resources.wx_gui import (
gui_sys_patch,
gui_support,
gui_help,
gui_macos_installer,
)
from resources import constants
@@ -93,7 +94,13 @@ class MainMenu(wx.Frame):
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):
pass

View File

@@ -20,6 +20,7 @@ from resources.sys_patch import (
from resources.wx_gui import (
gui_main_menu,
gui_support,
gui_download,
)
class SysPatchMenu(wx.Frame):
@@ -89,46 +90,23 @@ class SysPatchMenu(wx.Frame):
# KDK is already downloaded
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)
progress_bar.SetValue(0)
# Set below developer note
progress_bar.SetPosition(
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
subheader.SetLabel("Checking if checksum is valid...")
subheader.Center(wx.HORIZONTAL)
wx.GetApp().Yield()
if self.kdk_obj.validate_kdk_checksum() is False:
logging.error("KDK checksum validation failed")