package gui // startupGui.go implements the app startup pages import "fyne.io/fyne/v2" import "fyne.io/fyne/v2/app" import "fyne.io/fyne/v2/widget" import "fyne.io/fyne/v2/container" import "fyne.io/fyne/v2/canvas" import "fyne.io/fyne/v2/layout" import "fyne.io/fyne/v2/dialog" import "fyne.io/fyne/v2/theme" import "seekia/resources/imageFiles" import "seekia/internal/appMemory" import "seekia/internal/badgerDatabase" import "seekia/internal/localFilesystem" import "seekia/internal/helpers" import "seekia/internal/appUsers" import "seekia/internal/mySeedPhrases" import "seekia/internal/globalSettings" import "seekia/internal/translation" import "seekia/internal/imagery" import "seekia/internal/backgroundJobs" import "time" import "errors" import "slices" func StartGui(){ app := app.New() window := app.NewWindow("Seekia") windowSize := fyne.NewSize(600, 600) window.Resize(windowSize) window.CenterOnScreen() err := localFilesystem.InitializeAppDatastores() if (err != nil){ getThemeObject := func()fyne.Theme{ themeObject, err := getCustomFyneTheme("Light") if (err != nil){ return theme.LightTheme() } return themeObject } themeObject := getThemeObject() app.Settings().SetTheme(themeObject) errorToShow := errors.New("Seekia cannot access the filesystem: " + err.Error()) setErrorEncounteredPage_NoNavBar(window, errorToShow, false, nil) window.ShowAndRun() return } getAppTheme := func()(fyne.Theme, error){ getAppThemeName := func()(string, error){ exists, currentAppTheme, err := globalSettings.GetSetting("AppTheme") if (err != nil){ return "", errors.New("Seekia cannot access the filesystem: " + err.Error()) } if (exists == false){ return "Light", nil } return currentAppTheme, nil } appThemeName, err := getAppThemeName() if (err != nil) { return nil, err } themeObject, err := getCustomFyneTheme(appThemeName) if (err != nil){ return nil, err } return themeObject, nil } appTheme, err := getAppTheme() if (err != nil){ getThemeObject := func()fyne.Theme{ themeObject, err := getCustomFyneTheme("Light") if (err != nil){ return theme.LightTheme() } return themeObject } themeObject := getThemeObject() app.Settings().SetTheme(themeObject) setErrorEncounteredPage_NoNavBar(window, err, false, nil) window.ShowAndRun() return } app.Settings().SetTheme(appTheme) window.SetCloseIntercept(func(){ appMemory.SetMemoryEntry("CurrentViewedPage", "Shutdown") description1 := getBoldLabelCentered("Shutting Down...") progressBar := getWidgetCentered(widget.NewProgressBarInfinite()) description2 := getItalicLabelCentered("Seekia is shutting down background tasks.") emptyLabel := widget.NewLabel("") forceCloseButton := getWidgetCentered(widget.NewButtonWithIcon("Force Close", theme.CancelIcon(), func(){ window.Close() })) //TODO: Show the number of background tasks we are waiting to stop, along with their names page := container.NewVBox(layout.NewSpacer(), description1, progressBar, description2, emptyLabel, forceCloseButton, layout.NewSpacer()) stopJobsAndCloseWindowFunction := func(){ //TODO: Remove this time.Sleep once we actually have background tasks to close time.Sleep(time.Second) backgroundJobs.StopBackgroundJobs() badgerDatabase.StopDatabase() //TODO: Add peer node, peer server, manual broadcasts, manual downloads // We should also use goroutines to start the stop function for all background tasks // This way they can all gracefully shutdown simultaneously while we wait for them to stop window.Close() } window.SetContent(page) go stopJobsAndCloseWindowFunction() }) //TODO: On first startup, first show Choose Language page. // Then show page describing legal risks of Seekia, including information about countries where Tor is illegal. // It should not be a generic warning that users will not read. // Users should actually stop if Seekia use is illegal and puts them at risk of prosecution. setChooseAppUserPage(window) window.ShowAndRun() } func setChooseAppUserPage(window fyne.Window){ currentPage := func(){setChooseAppUserPage(window)} logoFileBytes := imageFiles.PNG_SeekiaLogo logoGoImage, err := imagery.ConvertPNGImageFileBytesToGolangImage(logoFileBytes) if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } seekiaLogo := canvas.NewImageFromImage(logoGoImage) seekiaLogo.FillMode = canvas.ImageFillContain logoSize := getCustomFyneSize(30) seekiaLogo.SetMinSize(logoSize) seekiaDescriptionLabel := getItalicLabelCentered("Be genetics aware.") selectLanguageTitle := getBoldLabel("Language:") currentLanguage := translation.GetMyLanguage() selectLanguageButton := getWidgetCentered(widget.NewButton(currentLanguage, func(){ setSelectLanguagePage(window, false, currentPage) })) selectLanguageRow := container.NewHBox(layout.NewSpacer(), selectLanguageTitle, selectLanguageButton, layout.NewSpacer()) spacerA := widget.NewLabel("") spacerB := widget.NewLabel("") page := container.NewVBox(spacerA, spacerB, seekiaLogo, seekiaDescriptionLabel, widget.NewSeparator(), selectLanguageRow, widget.NewSeparator()) allUsersList, err := appUsers.GetAppUsersList() if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } if (len(allUsersList) != 0){ selectUserLabel := getBoldLabelCentered("Select User:") page.Add(selectUserLabel) userButtonsGrid := container.NewGridWithColumns(1) for _, userName := range allUsersList{ selectUserButton := widget.NewButtonWithIcon(userName, theme.AccountIcon(), func(){ setLoadingScreen(window, translate("Starting Up"), translate("Loading...")) err := appUsers.SignInToAppUser(userName, true) if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } setHomePage(window) }) selectUserButton.Importance = widget.HighImportance userButtonsGrid.Add(selectUserButton) } userButtonsGridCentered := getContainerCentered(userButtonsGrid) page.Add(userButtonsGridCentered) page.Add(widget.NewSeparator()) } createUserButton := widget.NewButtonWithIcon("Create User", theme.ContentAddIcon(), func(){ setCreateAppUserPage(window, currentPage, currentPage) }) if (len(allUsersList) == 0){ createUserButtonCentered := getWidgetCentered(createUserButton) page.Add(createUserButtonCentered) } else { renameUserButton := widget.NewButtonWithIcon("Rename User", theme.DocumentCreateIcon(), func(){ setRenameAppUserPage(window, currentPage, currentPage) }) deleteUserButton := widget.NewButtonWithIcon("Delete User", theme.DeleteIcon(), func(){ setDeleteAppUserPage(window, currentPage, currentPage) }) userActionButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, createUserButton, renameUserButton, deleteUserButton)) page.Add(userActionButtonsGrid) page.Add(widget.NewSeparator()) } page.Add(layout.NewSpacer()) pageScrollable := container.NewVScroll(page) window.SetContent(pageScrollable) } func setCreateAppUserPage(window fyne.Window, previousPage func(), afterCreatePage func()){ currentPage := func(){setCreateAppUserPage(window, previousPage, afterCreatePage)} title := getPageTitleCentered("Create User") backButton := getBackButtonCentered(previousPage) userHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setAppUserExplainerPage(window, currentPage) }) description1 := getBoldLabel("Create a Seekia user.") description1Row := container.NewHBox(layout.NewSpacer(), description1, userHelpButton, layout.NewSpacer()) description2 := widget.NewLabel("This name will not be shared publicly.") nameEntry := widget.NewEntry() nameEntry.PlaceHolder = "Enter name..." nameEntryBoxed := getWidgetBoxed(nameEntry) description2WithEntry := getContainerCentered(container.NewGridWithColumns(1, description2, nameEntryBoxed)) createUserButton := getWidgetCentered(widget.NewButtonWithIcon("Create", theme.ConfirmIcon(), func(){ newUserName := nameEntry.Text if (newUserName == ""){ dialogTitle := translate("User Name Is Empty") dialogMessageA := getLabelCentered(translate("The user name is empty.")) dialogMessageB := getLabelCentered(translate("Enter a name.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } if (len(newUserName) > 30){ dialogTitle := translate("User Name Is Too Long") dialogMessageA := getLabelCentered(translate("The user name is too long.")) dialogMessageB := getLabelCentered(translate("It cannot exceed 30 characters.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } isAllowed := appUsers.VerifyAppUserNameCharactersAreAllowed(newUserName) if (isAllowed == false){ dialogTitle := translate("User Name Is Not Allowed") dialogMessageA := getLabelCentered(translate("The user name is not allowed.")) dialogMessageB := getLabelCentered(translate("It must contain only numbers and letters.")) dialogMessageC := getLabelCentered(translate("It cannot contain any spaces.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } alreadyExists, err := appUsers.CreateAppUser(newUserName) if (err != nil) { setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } if (alreadyExists == true){ dialogTitle := translate("User Name Is A Duplicate.") dialogMessageA := getLabelCentered(translate("You already have a user with this name.")) dialogMessageB := getLabelCentered(translate("Enter a unique name.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } afterCreatePage() })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, description2WithEntry, createUserButton) window.SetContent(page) } func setAppUserExplainerPage(window fyne.Window, previousPage func()){ title := getPageTitleCentered("Help - App User") backButton := getBackButtonCentered(previousPage) description1 := getBoldLabelCentered("To use Seekia, you must create an app user.") description2 := getLabelCentered("Each app user has its own data folder.") description3 := getLabelCentered("Each user can create their own Mate, Host, and Moderator identity.") description4 := getLabelCentered("This allows you to create multiple identities and switch between them.") description5 := getLabelCentered("For example, you could operate 2 moderator identities and switch between them.") description6 := getLabelCentered("Most users will only need to create 1 user.") description7 := getLabelCentered("Your user names are never uploaded or shared anywhere.") description8 := getLabelCentered("You must export each user's data when transferring to a new device.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) window.SetContent(page) } func setRenameAppUserPage(window fyne.Window, previousPage func(), afterRenamePage func()){ currentPage := func(){setRenameAppUserPage(window, previousPage, afterRenamePage)} title := getPageTitleCentered("Rename User") backButton := getBackButtonCentered(previousPage) allUsersList, err := appUsers.GetAppUsersList() if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, previousPage) return } if (len(allUsersList) == 0){ setErrorEncounteredPage_NoNavBar(window, errors.New("setRenameAppUserPage called when no users exist."), true, previousPage) return } selectUserLabel := getBoldLabelCentered("Select User:") userSelector := widget.NewSelect(allUsersList, nil) userSelector.PlaceHolder = "Select user..." userSelectorCentered := getWidgetCentered(userSelector) enterNewNameLabel := getBoldLabel("Enter New Name:") newNameEntry := widget.NewEntry() newNameEntry.SetPlaceHolder("Enter new name...") newNameEntryBoxed := getWidgetBoxed(newNameEntry) newNameEntryWithLabel := getContainerCentered(container.NewGridWithColumns(1, enterNewNameLabel, newNameEntryBoxed)) renameButton := getWidgetCentered(widget.NewButtonWithIcon("Rename", theme.ConfirmIcon(), func(){ currentUserNameSelectedIndex := userSelector.SelectedIndex() if (currentUserNameSelectedIndex == -1){ dialogTitle := translate("No User Selected") dialogMessageA := getLabelCentered(translate("No user is selected.")) dialogMessageB := getLabelCentered(translate("Select the user you want to rename.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } currentUserName := userSelector.Selected newUserName := newNameEntry.Text if (newUserName == ""){ dialogTitle := translate("User Name Is Empty") dialogMessageA := getLabelCentered(translate("The user name is empty.")) dialogMessageB := getLabelCentered(translate("Enter a name.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } if (len(newUserName) > 30){ dialogTitle := translate("User Name Is Too Long") dialogMessageA := getLabelCentered(translate("The user name is too long.")) dialogMessageB := getLabelCentered(translate("It cannot exceed 30 characters.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } isAllowed := appUsers.VerifyAppUserNameCharactersAreAllowed(newUserName) if (isAllowed == false){ dialogTitle := translate("User Name Is Not Allowed") dialogMessageA := getLabelCentered(translate("The user name is not allowed.")) dialogMessageB := getLabelCentered(translate("It must contain only numbers and letters.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } alreadyExists := slices.Contains(allUsersList, newUserName) if (alreadyExists == true){ dialogTitle := translate("User Name Is A Duplicate.") dialogMessageA := getLabelCentered(translate("You already have a user with this name.")) dialogMessageB := getLabelCentered(translate("Enter a unique name.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } setLoadingScreen(window, translate("Rename User"), translate("Renaming User...")) userFound, err := appUsers.RenameAppUser(currentUserName, newUserName) if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } if (userFound == false){ setErrorEncounteredPage_NoNavBar(window, errors.New("App user not found after being found already."), true, currentPage) return } afterRenamePage() })) page := container.NewVBox(title, backButton, widget.NewSeparator(), selectUserLabel, userSelectorCentered, newNameEntryWithLabel, renameButton) window.SetContent(page) } func setDeleteAppUserPage(window fyne.Window, previousPage func(), afterDeletePage func()){ currentPage := func(){setDeleteAppUserPage(window, previousPage, afterDeletePage)} title := getPageTitleCentered("Delete User") backButton := getBackButtonCentered(previousPage) description := getBoldLabelCentered("Choose the user to delete.") allUsersList, err := appUsers.GetAppUsersList() if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, previousPage) return } if (len(allUsersList) == 0){ setErrorEncounteredPage_NoNavBar(window, errors.New("setDeleteAppUserPage called when no users exist."), true, previousPage) return } userButtonsGrid := container.NewGridWithColumns(1) for _, userName := range allUsersList{ selectUserButton := widget.NewButtonWithIcon(userName, theme.AccountIcon(), func(){ setConfirmDeleteAppUserPage(window, userName, currentPage, afterDeletePage) }) userButtonsGrid.Add(selectUserButton) } userButtonsGridCentered := getContainerCentered(userButtonsGrid) page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), userButtonsGridCentered) window.SetContent(page) } func setConfirmDeleteAppUserPage(window fyne.Window, userName string, previousPage func(), afterDeletePage func()){ setLoadingScreen(window, translate("Delete User"), translate("Loading Delete User Page...")) currentPage := func(){setConfirmDeleteAppUserPage(window, userName, previousPage, afterDeletePage)} title := getPageTitleCentered("Delete User") backButton := getBackButtonCentered(previousPage) // We sign in to the user to see if there are any user identities getNumberOfUserIdentities := func()(int, error){ err := appUsers.SignInToAppUser(userName, false) if (err != nil){ return 0, err } numberOfIdentities := 0 mateIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Mate") if (err != nil) { return 0, err } if (mateIdentityExists == true){ numberOfIdentities += 1 } hostIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Host") if (err != nil) { return 0, err } if (hostIdentityExists == true){ numberOfIdentities += 1 } moderatorIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Moderator") if (err != nil) { return 0, err } if (moderatorIdentityExists == true){ numberOfIdentities += 1 } err = appUsers.SignOutOfAppUser() if (err != nil){ return 0, err } return numberOfIdentities, nil } numberOfIdentities, err := getNumberOfUserIdentities() if (err != nil) { setErrorEncounteredPage_NoNavBar(window, err, true, previousPage) return } if (numberOfIdentities != 0){ getNumberOfIdentitiesDescription := func()string{ if (numberOfIdentities == 1){ return "This user has 1 identity." } numberOfIdentitiesString := helpers.ConvertIntToString(numberOfIdentities) return "This user has " + numberOfIdentitiesString + " identities." } numberOfIdentitiesDescription := getNumberOfIdentitiesDescription() description1 := getBoldLabelCentered(numberOfIdentitiesDescription) description2 := getLabelCentered("You must delete all user identities before deleting the user.") description3 := getLabelCentered("Delete your identities on the Settings - My Data - Delete Identity page.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) window.SetContent(page) return } description1 := getBoldLabelCentered("Are you sure you want to delete this user?") nameLabel := widget.NewLabel("Name:") userNameLabel := getBoldLabel(userName) userNameRow := container.NewHBox(layout.NewSpacer(), nameLabel, userNameLabel, layout.NewSpacer()) description2 := getLabelCentered("This will delete all of your user data.") description3 := getLabelCentered("This includes your desires, genetic analyses, genomes, and settings.") description4 := getLabelCentered("Export your data on the Settings - My Data - Export Data page to retain your data.") deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ setLoadingScreen(window, translate("Delete User"), translate("Deleting User...")) userExists, err := appUsers.DeleteAppUser(userName) if (err != nil){ setErrorEncounteredPage_NoNavBar(window, err, true, currentPage) return } if (userExists == false){ setErrorEncounteredPage_NoNavBar(window, errors.New("setConfirmDeleteAppUserPage called when user does not exist."), true, currentPage) return } afterDeletePage() })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, userNameRow, description2, description3, description4, deleteButton) window.SetContent(page) }