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