seekia/gui/startupGui.go

625 lines
20 KiB
Go
Raw Normal View History

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