seekia/gui/downloadGui.go

438 lines
16 KiB
Go

package gui
// downloadGui.go implements pages to monitor manual downloads
// These are downloads whose status and progress the user is able to monitor
import "fyne.io/fyne/v2"
import "fyne.io/fyne/v2/widget"
import "fyne.io/fyne/v2/theme"
import "fyne.io/fyne/v2/container"
import "fyne.io/fyne/v2/data/binding"
import "seekia/internal/helpers"
import "seekia/internal/network/appNetworkType/getAppNetworkType"
import "seekia/internal/network/manualDownloads"
import "seekia/internal/appMemory"
import "errors"
import "time"
func setDownloadMissingUserProfilePage(window fyne.Window, profileAuthorIdentityHash [16]byte, getViewableOnly bool, stopDownloadOnPageExit bool, previousPage func(), nextPage func(), exitPage func()){
pageTitleText := "Download User Profile"
description1Text := "The user's profile is missing."
//Outputs:
// -bool: Any hosts found
// -[][23]byte: Download identifiers list
// -error
startNewDownloadFunction := func()(bool, [][23]byte, error){
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
if (err != nil) { return false, nil, err }
anyHostsFound, processIdentifier, err := manualDownloads.StartNewestUserProfileDownload(profileAuthorIdentityHash, appNetworkType, getViewableOnly, 1, 10)
if (err != nil) { return false, nil, err }
if (anyHostsFound == false){
emptyList := make([][23]byte, 0)
return false, emptyList, nil
}
processIdentifiersList := [][23]byte{processIdentifier}
return true, processIdentifiersList, nil
}
anyHostsFound, processIdentifiersList, err := startNewDownloadFunction()
if (err != nil){
setErrorEncounteredPage(window, err, previousPage)
return
}
noHostsFound := !anyHostsFound
setMonitorManualDownloadsPage(window, pageTitleText, "Profile", description1Text, noHostsFound, processIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, 1, previousPage, nextPage, exitPage)
}
//Inputs:
// -fyne.Window:
// -string
// -string
// -string
// -bool: True if no hosts are found
// -[][23]byte: List of process identifiers to monitor
// -func()(bool, [][23]byte, error): Function to retry all downloads
// -bool: Any hosts found
// -[][23]byte: New process identifiers of new download
// -error
// -bool: Stop download on page exit
// -int: expectedSuccessfulDownloadsPerProcess
// -func(): The page to visit if back button is pressed.
// -func(): The page to visit after a successful download
// -func()
func setMonitorManualDownloadsPage(window fyne.Window,
pageTitleText string,
downloadType string,
description1Text string,
noHostsFound bool,
processIdentifiersList [][23]byte,
startNewDownloadFunction func()(bool, [][23]byte, error),
stopDownloadOnPageExit bool,
expectedSuccessfulDownloadsPerProcess int,
previousPage func(),
afterCompletionPage func(),
exitPage func()){
currentPage := func(){setMonitorManualDownloadsPage(window, pageTitleText, downloadType, description1Text, noHostsFound, processIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, expectedSuccessfulDownloadsPerProcess, previousPage, afterCompletionPage, exitPage)}
title := getPageTitleCentered(pageTitleText)
previousPageWithCancel := func(){
if (stopDownloadOnPageExit == true && noHostsFound == false){
for _, processIdentifier := range processIdentifiersList{
manualDownloads.EndProcess(processIdentifier)
}
}
appMemory.DeleteMemoryEntry("CurrentViewedPage")
previousPage()
}
if (downloadType != "Profile" && downloadType != "Message"){
//TODO: Replace downloadType with custom descriptions for missing hosts, download in progress, and content may not exist
setErrorEncounteredPage(window, errors.New("setMonitorProfileDownloadPage called with invalid downloadType: " + downloadType), previousPageWithCancel)
return
}
backButton := getBackButtonCentered(previousPageWithCancel)
pageIdentifier, err := helpers.GetNewRandomHexString(16)
if (err != nil) {
setErrorEncounteredPage(window, err, previousPageWithCancel)
return
}
appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier)
checkIfPageHasChangedFunction := func()bool{
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
if (exists == true && currentViewedPage == pageIdentifier){
return false
}
return true
}
description1 := getLabelCentered(description1Text)
retryFunction := func(){
newDownloadAnyHostsFound, newProcessIdentifiersList, err := startNewDownloadFunction()
if (err != nil){
setErrorEncounteredPage(window, err, previousPageWithCancel)
return
}
newDownloadNoHostsFound := !newDownloadAnyHostsFound
setMonitorManualDownloadsPage(window, pageTitleText, downloadType, description1Text, newDownloadNoHostsFound, newProcessIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, expectedSuccessfulDownloadsPerProcess, previousPage, afterCompletionPage, exitPage)
}
if (noHostsFound == true){
description2 := getLabelCentered(downloadType + " download failed because no available hosts were found.")
description3 := getLabelCentered("Please wait for Seekia to find more hosts.")
description4 := getLabelCentered("This should take less than 1 minute.")
retryingInSecondsBinding := binding.NewString()
startRetryCountdownFunction := func(){
secondsRemaining := 30
for {
secondsRemainingString := helpers.ConvertIntToString(secondsRemaining)
if (secondsRemaining != 1){
retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " seconds...")
} else {
retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " second...")
}
time.Sleep(time.Second)
secondsRemaining -= 1
if (secondsRemaining <= 0){
pageHasChanged := checkIfPageHasChangedFunction()
if (pageHasChanged == true){
return
}
retryFunction()
return
}
}
}
retryingInLabel := widget.NewLabelWithData(retryingInSecondsBinding)
retryingInLabel.TextStyle = getFyneTextStyle_Bold()
retryingInLabelCentered := getWidgetCentered(retryingInLabel)
retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction))
exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))
manageConnectionDescription := getLabelCentered("Check if your internet connection is working below.")
manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){
setManageNetworkConnectionPage(window, currentPage)
}))
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), retryingInLabelCentered, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), manageConnectionDescription, manageConnectionButton)
setPageContent(page, window)
go startRetryCountdownFunction()
return
}
downloadProgressStatusBinding := binding.NewString()
downloadNumHostsContactedBinding := binding.NewString()
downloadProgressDetailsBinding := binding.NewString()
updateBindingsFunction := func(){
startTime := time.Now().Unix()
setDownloadProgressStatus := func(processComplete bool, newStatus string){
getProgressEllipsis := func()string{
if (processComplete == true){
return ""
}
currentTime := time.Now().Unix()
secondsElapsed := currentTime - startTime
if (secondsElapsed % 3 == 0){
return "."
}
if (secondsElapsed % 3 == 1){
return ".."
}
return "..."
}
progressEllipsis := getProgressEllipsis()
downloadProgressStatusBinding.Set(newStatus + progressEllipsis)
}
// Map Structure: Process identifier -> Progress details
processLatestProgressDetailsMap := make(map[[23]byte]string)
// Map Structure: Process identifier -> Progress details last update time
processLatestUpdatedTimesMap := make(map[[23]byte]int64)
for {
// We use the below function to combine the stats from all processes, if multiple processes exist.
//Outputs:
// -bool: Download is complete
// -bool: Download ran out of hosts
// -int: Number of successful downloads
// -int: Number of hosts missing content
// -string: Download progress details
// -error
getDownloadInfo := func()(bool, bool, int, int, string, error){
allProcessesAreComplete := false
// This will sum all the successful downloads for all processes
numberOfSuccessfulDownloads := 0
// This will sum all of the number of hosts missing content for all processes
numberOfHostsMissingContent := 0
for _, processIdentifier := range processIdentifiersList{
processFound, processIsComplete, processEncounteredError, processError, processNumberOfSuccessfulDownloads, processNumberOfHostsMissingContent, processProgressDetails := manualDownloads.GetProcessInfo(processIdentifier)
if (processFound == false){
// This should not happen
return false, false, 0, 0, "", errors.New("Download process not found.")
}
if (processIsComplete == true && processEncounteredError == true){
return true, false, 0, 0, "", processError
}
if (processIsComplete == true && processNumberOfSuccessfulDownloads == 0){
// Process failed and ran out of hosts
return true, true, 0, 0, "", nil
}
numberOfSuccessfulDownloads += processNumberOfSuccessfulDownloads
numberOfHostsMissingContent += processNumberOfHostsMissingContent
if (processIsComplete == false){
allProcessesAreComplete = false
}
latestDetails, exists := processLatestProgressDetailsMap[processIdentifier]
if (exists == true && latestDetails == processProgressDetails){
// This process has not had a new details update
// We can skip it
continue
}
// This process has new details
processLatestProgressDetailsMap[processIdentifier] = processProgressDetails
currentTime := time.Now().Unix()
processLatestUpdatedTimesMap[processIdentifier] = currentTime
}
// Now we find the newest status to show to the user
newestProgressDetails := ""
newestDetailsUpdatedTime := int64(0)
for index, processIdentifier := range processIdentifiersList{
latestDetailsUpdateTime, exists := processLatestUpdatedTimesMap[processIdentifier]
if (exists == false){
// This should not happen
// All processes should be added to this map during our first iteration through processes details
return false, false, 0, 0, "", errors.New("processLatestUpdatedTimesMap missing process latest updated time")
}
if (index == 0 || latestDetailsUpdateTime > newestDetailsUpdatedTime){
latestProcessDetails, exists := processLatestProgressDetailsMap[processIdentifier]
if (exists == false){
// This should not happen
// All processes should be added to this map during our first iteration through processes details
return false, false, 0, 0, "", errors.New("processLatestProgressDetailsMap missing process details")
}
newestProgressDetails = latestProcessDetails
}
}
return allProcessesAreComplete, false, numberOfSuccessfulDownloads, numberOfHostsMissingContent, newestProgressDetails, nil
}
downloadIsComplete, downloadRanOutOfHosts, numberOfSuccessfulDownloads, numberOfHostsMissingContent, downloadProgressDetails, err := getDownloadInfo()
if (err != nil){
setDownloadProgressStatus(true, "ERROR: " + err.Error())
downloadProgressDetailsBinding.Set("Report this error to the Seekia developers.")
return
}
numberOfHostsContacted := numberOfSuccessfulDownloads + numberOfHostsMissingContent
numberOfHostsContactedString := helpers.ConvertIntToString(numberOfHostsContacted)
if (downloadIsComplete == true){
// Download is complete.
pageHasChanged := checkIfPageHasChangedFunction()
if (pageHasChanged == true){
return
}
expectedSuccessfulDownloads := len(processIdentifiersList) * expectedSuccessfulDownloadsPerProcess
if (numberOfSuccessfulDownloads >= expectedSuccessfulDownloads){
// We downloaded the content we wanted. Nothing left to do.
afterCompletionPage()
return
}
// Download did not get required content.
// We will show user option to retry.
retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction))
exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))
checkConnectionDescription := getLabelCentered("Check if your internet connection is working below.")
manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){
setManageNetworkConnectionPage(window, currentPage)
}))
if (downloadRanOutOfHosts == true){
description2 := getLabelCentered("The download was unsuccessful.")
description3 := getLabelCentered("All the hosts we contacted failed to respond.")
description4 := getLabelCentered("You can exit or wait for more hosts to be found and retry.")
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, retryButton, exitButton, widget.NewSeparator(), checkConnectionDescription, manageConnectionButton)
setPageContent(page, window)
return
}
description2 := getLabelCentered("The download was unsuccessful.")
description3 := getLabelCentered("We contacted " + numberOfHostsContactedString + " hosts.")
description4 := getLabelCentered("The " + downloadType + " may not exist on the network.")
description5 := getLabelCentered("Retry the download?")
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, description5, retryButton, exitButton, widget.NewSeparator(), checkConnectionDescription, manageConnectionButton)
setPageContent(page, window)
return
}
// Download is not complete
numberOfSuccessfulDownloadsString := helpers.ConvertIntToString(numberOfSuccessfulDownloads)
progressProgressStatusString := "Downloaded from " + numberOfSuccessfulDownloadsString + " hosts."
setDownloadProgressStatus(downloadIsComplete, progressProgressStatusString)
downloadProgressDetailsBinding.Set(downloadProgressDetails)
downloadNumHostsContactedBinding.Set("Contacted " + numberOfHostsContactedString + " hosts.")
pageHasChanged := checkIfPageHasChangedFunction()
if (pageHasChanged == true){
if (stopDownloadOnPageExit == true){
for _, processIdentifier := range processIdentifiersList{
manualDownloads.EndProcess(processIdentifier)
}
}
return
}
time.Sleep(200 * time.Millisecond)
}
}
description2 := getBoldLabelCentered("Seekia is attempting to download the " + downloadType)
description3 := getLabelCentered("The status of the download is displayed below.")
downloadProgressStatusLabel := widget.NewLabelWithData(downloadProgressStatusBinding)
downloadProgressStatusLabel.TextStyle = getFyneTextStyle_Bold()
downloadProgressStatusLabelCentered := getWidgetCentered(downloadProgressStatusLabel)
downloadNumHostsContactedLabel := getWidgetCentered(widget.NewLabelWithData(downloadNumHostsContactedBinding))
downloadProgressDetailsLabel := getWidgetCentered(widget.NewLabelWithData(downloadProgressDetailsBinding))
exitPageButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.MediaSkipNextIcon(), func(){
for _, processIdentifier := range processIdentifiersList{
manualDownloads.EndProcess(processIdentifier)
}
appMemory.DeleteMemoryEntry("CurrentViewedPage")
exitPage()
}))
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), downloadProgressStatusLabelCentered, downloadNumHostsContactedLabel, downloadProgressDetailsLabel, widget.NewSeparator(), exitPageButton)
setPageContent(page, window)
go updateBindingsFunction()
}