From f085af4e81646d15f48edd82cea654d0831c18bf Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 11:37:00 +0100 Subject: [PATCH 01/16] Initial commit of ui rewrite --- go.mod | 1 + go.sum | 2 + internal/ui/about.go | 71 ---------- internal/ui/recv.go | 97 +------------ internal/ui/send.go | 97 +------------ internal/ui/settings.go | 290 --------------------------------------- internal/ui/setup.go | 31 +++++ internal/ui/tabs.go | 90 ------------ internal/ui/tabs_test.go | 28 ---- 9 files changed, 38 insertions(+), 669 deletions(-) delete mode 100644 internal/ui/about.go delete mode 100644 internal/ui/settings.go create mode 100644 internal/ui/setup.go delete mode 100644 internal/ui/tabs.go delete mode 100644 internal/ui/tabs_test.go diff --git a/go.mod b/go.mod index 7773c209..4cff2254 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( fyne.io/fyne/v2 v2.5.3 + fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763 github.com/fynelabs/fyneselfupdate v0.1.1 github.com/fynelabs/selfupdate v0.2.0 github.com/klauspost/compress v1.17.11 diff --git a/go.sum b/go.sum index 60975913..9c961f4c 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ fyne.io/fyne/v2 v2.5.3 h1:k6LjZx6EzRZhClsuzy6vucLZBstdH2USDGHSGWq8ly8= fyne.io/fyne/v2 v2.5.3/go.mod h1:0GOXKqyvNwk3DLmsFu9v0oYM0ZcD1ysGnlHCerKoAmo= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763 h1:eKeSmgu0Ege3moDYC7ZqYB7ypOgiUzV/cyt1GAAJ/A8= +fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763/go.mod h1:pWJAjc53T8QL7CBpByv7zFUI4Au5Pbh6JC30eViEHVU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= diff --git a/internal/ui/about.go b/internal/ui/about.go deleted file mode 100644 index 296dc30c..00000000 --- a/internal/ui/about.go +++ /dev/null @@ -1,71 +0,0 @@ -package ui - -import ( - "net/url" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/util" -) - -func newAboutTab(app fyne.App) *container.TabItem { - const version = "v3.7.0" - - repoURL := util.URLToGitHubProject("") - icon := newClickableIcon(app.Icon(), repoURL, app) - - nameLabel := newBoldLabel("Rymdport") - spacerLabel := newBoldLabel("-") - - releaseURL := util.URLToGitHubProject("/releases/tag/" + version) - hyperlink := &widget.Hyperlink{Text: version, URL: releaseURL, TextStyle: fyne.TextStyle{Bold: true}} - - spacer := &layout.Spacer{} - content := container.NewVBox( - spacer, - container.NewHBox(spacer, icon, spacer), - container.NewHBox( - spacer, - nameLabel, - spacerLabel, - hyperlink, - spacer, - ), - spacer, - ) - - return &container.TabItem{ - Text: "About", - Icon: theme.InfoIcon(), - Content: content, - } -} - -type clickableIcon struct { - widget.Icon - app fyne.App - url *url.URL -} - -func (c *clickableIcon) Tapped(_ *fyne.PointEvent) { - err := c.app.OpenURL(c.url) - if err != nil { - fyne.LogError("Failed to open repository: ", err) - } -} - -func (c *clickableIcon) Cursor() desktop.Cursor { - return desktop.PointerCursor -} - -func (c *clickableIcon) MinSize() fyne.Size { - return fyne.Size{Width: 256, Height: 256} -} - -func newClickableIcon(res fyne.Resource, url *url.URL, app fyne.App) *clickableIcon { - return &clickableIcon{Icon: widget.Icon{Resource: res}, app: app, url: url} -} diff --git a/internal/ui/recv.go b/internal/ui/recv.go index 82e320c0..afa00629 100644 --- a/internal/ui/recv.go +++ b/internal/ui/recv.go @@ -2,102 +2,9 @@ package ui import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/completion" - "github.com/Jacalz/rymdport/v3/internal/transport" - "github.com/Jacalz/rymdport/v3/internal/transport/bridge" - "github.com/Jacalz/rymdport/v3/internal/util" ) -type recv struct { - codeEntry *completionEntry - data bridge.RecvData - - window fyne.Window -} - -func newRecvTab(w fyne.Window, c *transport.Client) *container.TabItem { - recv := &recv{window: w} - - return &container.TabItem{ - Text: "Receive", - Icon: theme.DownloadIcon(), - Content: recv.buildUI(c), - } -} - -func (r *recv) buildUI(client *transport.Client) *fyne.Container { - r.codeEntry = newCompletionEntry(client, r.window.Canvas(), client.App) - r.codeEntry.OnSubmitted = func(_ string) { r.onRecv() } - - codeButton := &widget.Button{Text: "Receive", Icon: theme.DownloadIcon(), OnTapped: r.onRecv} - - r.data = bridge.RecvData{Client: client, Window: r.window} - - box := container.NewVBox(&widget.Separator{}, container.NewGridWithColumns(2, r.codeEntry, codeButton), &widget.Separator{}) - return container.NewBorder(box, nil, nil, nil, r.data.NewRecvList()) -} - -func (r *recv) onRecv() { - if err := r.codeEntry.Validate(); err != nil || r.codeEntry.Text == "" { - dialog.ShowInformation("Invalid code", "The code is invalid. Please try again.", r.window) - return - } - - r.data.NewReceive(r.codeEntry.Text) - r.codeEntry.SetText("") -} - -type completionEntry struct { - widget.Entry - driver desktop.Driver - canvas fyne.Canvas - complete completion.TabCompleter -} - -// AcceptsTab overrides tab handling to allow tabs as input. -func (c *completionEntry) AcceptsTab() bool { - return true -} - -// TypedKey adapts the key inputs to handle tab completion. -func (c *completionEntry) TypedKey(key *fyne.KeyEvent) { - switch key.Name { - case desktop.KeyShiftLeft, desktop.KeyShiftRight: - case fyne.KeyTab: - if c.driver.CurrentKeyModifiers()&fyne.KeyModifierShift != 0 { - c.setCompletion(c.complete.Previous) - return - } - - c.setCompletion(c.complete.Next) - case fyne.KeyEscape: - c.canvas.Unfocus() - default: - c.complete.Reset() - c.Entry.TypedKey(key) - } -} - -func (c *completionEntry) setCompletion(lookup func(string) string) { - text := lookup(c.Text) - c.CursorColumn = len(text) - c.SetText(text) -} - -func newCompletionEntry(client *transport.Client, canvas fyne.Canvas, app fyne.App) *completionEntry { - entry := &completionEntry{ - canvas: canvas, - driver: app.Driver().(desktop.Driver), - Entry: widget.Entry{ - PlaceHolder: "Enter code", Scroll: container.ScrollHorizontalOnly, Validator: util.CodeValidator, - }, - } - entry.complete.Generate = client.GenerateCodeCompletion - entry.ExtendBaseWidget(entry) - return entry +func createRecvPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { + return widget.NewLabel("Receive something") } diff --git a/internal/ui/send.go b/internal/ui/send.go index f522bc81..8b6b0b0a 100644 --- a/internal/ui/send.go +++ b/internal/ui/send.go @@ -2,102 +2,9 @@ package ui import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/transport" - "github.com/Jacalz/rymdport/v3/internal/transport/bridge" - "github.com/Jacalz/rymdport/v3/internal/util" ) -type send struct { - contentPicker *dialog.CustomDialog - fileDialog *dialog.FileDialog - directoryDialog *dialog.FileDialog - - data bridge.SendData - - client *transport.Client - window fyne.Window -} - -func newSend(w fyne.Window, c *transport.Client) *send { - return &send{client: c, window: w} -} - -func (s *send) newSendTab() *container.TabItem { - return &container.TabItem{ - Text: "Send", - Icon: theme.MailSendIcon(), - Content: s.buildUI(s.window), - } -} - -func (s *send) buildUI(window fyne.Window) *fyne.Container { - fileChoice := &widget.Button{Text: "File", Icon: theme.FileIcon(), OnTapped: s.onFileSend} - directoryChoice := &widget.Button{Text: "Directory", Icon: theme.FolderOpenIcon(), OnTapped: s.onDirSend} - textChoice := &widget.Button{Text: "Text", Icon: theme.DocumentCreateIcon(), OnTapped: s.onTextSend} - codeChoice := &widget.Check{Text: "Use a custom code", OnChanged: s.onCustomCode, Checked: s.client.CustomCode} - choiceContent := container.NewGridWithColumns(1, fileChoice, directoryChoice, textChoice, codeChoice) - s.contentPicker = dialog.NewCustom("Pick a content type", "Cancel", choiceContent, window) - - s.data = bridge.SendData{Client: s.client, Window: window} - contentToSend := &widget.Button{Text: "Add content to send", Icon: theme.ContentAddIcon(), OnTapped: func() { - codeChoice.SetChecked(s.client.CustomCode) - s.contentPicker.Show() - }} - - s.fileDialog = dialog.NewFileOpen(s.data.OnFileSelect, window) - s.directoryDialog = dialog.NewFolderOpen(s.data.OnDirSelect, window) - - box := container.NewVBox(&widget.Separator{}, contentToSend, &widget.Separator{}) - return container.NewBorder(box, nil, nil, nil, s.data.NewSendList()) -} - -func (s *send) onFileSend() { - s.contentPicker.Hide() - s.fileDialog.Resize(util.WindowSizeToDialog(s.window.Canvas().Size())) - s.fileDialog.Show() -} - -func (s *send) onDirSend() { - s.contentPicker.Hide() - s.fileDialog.Resize(util.WindowSizeToDialog(s.window.Canvas().Size())) - s.directoryDialog.Show() -} - -func (s *send) onTextSend() { - s.contentPicker.Hide() - s.data.SendText() -} - -func (s *send) onCustomCode(enabled bool) { - s.client.CustomCode = enabled -} - -func (s *send) newTransfer(uris []fyne.URI) { - if len(uris) == 0 { - return - } - - if len(uris) > 1 { - s.data.NewSendFromFiles(uris) - return - } - - isDir, err := storage.CanList(uris[0]) - if err != nil { - fyne.LogError("Could not check if path is directory", err) - return - } - - if !isDir { - reader, err := storage.Reader(uris[0]) - s.data.OnFileSelect(reader, err) - } else { - reader, err := storage.ListerForURI(uris[0]) - s.data.OnDirSelect(reader, err) - } +func createSendPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { + return widget.NewLabel("Send something") } diff --git a/internal/ui/settings.go b/internal/ui/settings.go deleted file mode 100644 index ab6bb1a7..00000000 --- a/internal/ui/settings.go +++ /dev/null @@ -1,290 +0,0 @@ -package ui - -import ( - "errors" - "os" - "path/filepath" - - "fyne.io/fyne/v2" - appearance "fyne.io/fyne/v2/cmd/fyne_settings/settings" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/transport" - "github.com/Jacalz/rymdport/v3/internal/updater" - "github.com/Jacalz/rymdport/v3/internal/util" - "github.com/rymdport/wormhole/wormhole" -) - -type settings struct { - downloadPathEntry *widget.Entry - overwriteFiles *widget.RadioGroup - notificationRadio *widget.RadioGroup - extractRadio *widget.RadioGroup - checkUpdatesRadio *widget.RadioGroup - - componentSlider *widget.Slider - componentLabel *widget.Label - verifyRadio *widget.RadioGroup - appID *widget.SelectEntry - rendezvousURL *widget.SelectEntry - transitRelayAddress *widget.SelectEntry - - client *transport.Client - preferences fyne.Preferences - window fyne.Window -} - -func newSettingsTab(w fyne.Window, c *transport.Client) *container.TabItem { - settings := &settings{window: w, client: c, preferences: c.App.Preferences()} - - return &container.TabItem{ - Text: "Settings", - Icon: theme.SettingsIcon(), - Content: settings.buildUI(c.App), - } -} - -func (s *settings) onDownloadsPathSubmitted(path string) { - path = filepath.Clean(path) - info, err := os.Stat(path) - if errors.Is(err, os.ErrNotExist) { - dialog.ShowInformation("Does not exist", "Please select a valid directory.", s.window) - return - } else if err != nil { - fyne.LogError("Error when trying to verify directory", err) - dialog.ShowError(err, s.window) - return - } else if !info.IsDir() { - dialog.ShowInformation("Not a directory", "Please select a valid directory.", s.window) - return - } - - s.client.DownloadPath = path - s.preferences.SetString("DownloadPath", s.client.DownloadPath) - s.downloadPathEntry.SetText(s.client.DownloadPath) -} - -func (s *settings) onDownloadsPathSelected() { - folder := dialog.NewFolderOpen(func(folder fyne.ListableURI, err error) { - if err != nil { - fyne.LogError("Error on selecting folder", err) - dialog.ShowError(err, s.window) - return - } else if folder == nil { - return - } - - s.client.DownloadPath = folder.Path() - s.preferences.SetString("DownloadPath", s.client.DownloadPath) - s.downloadPathEntry.SetText(s.client.DownloadPath) - }, s.window) - - folder.Resize(util.WindowSizeToDialog(s.window.Canvas().Size())) - folder.Show() -} - -func (s *settings) onOverwriteFilesChanged(selected string) { - if selected == "Off" { - s.client.OverwriteExisting = false - s.preferences.SetBool("OverwriteFiles", s.client.OverwriteExisting) - return - } - - confirm := dialog.NewConfirm("Are you sure?", "Enabling this option risks potentially overwriting important files.", func(enable bool) { - if !enable { - s.overwriteFiles.SetSelected("Off") - return - } - - s.client.OverwriteExisting = true - s.preferences.SetBool("OverwriteFiles", s.client.OverwriteExisting) - }, s.window) - confirm.SetConfirmImportance(widget.WarningImportance) - confirm.Show() -} - -func (s *settings) onNotificationsChanged(selected string) { - s.client.Notifications = selected == "On" - s.preferences.SetBool("Notifications", s.client.Notifications) -} - -func (s *settings) onExtractChanged(selected string) { - s.client.NoExtractDirectory = selected == "Off" // UI representation is flipped. - s.preferences.SetBool("NoExtractDirectory", s.client.NoExtractDirectory) -} - -func (s *settings) onCheckUpdatesChanged(selected string) { - s.preferences.SetBool("CheckUpdates", selected == "On") -} - -func (s *settings) onComponentsChange(value float64) { - s.componentLabel.SetText(string('0' + byte(value))) -} - -func (s *settings) onComponentsChangeEnded(value float64) { - s.client.PassPhraseComponentLength = int(value) - s.preferences.SetInt("ComponentLength", int(value)) -} - -func (s *settings) onAppIDChanged(appID string) { - s.client.AppID = appID - s.preferences.SetString("AppID", appID) -} - -func (s *settings) onRendezvousURLChange(url string) { - s.client.RendezvousURL = url - s.preferences.SetString("RendezvousURL", url) -} - -func (s *settings) onTransitAdressChange(address string) { - s.client.TransitRelayAddress = address - s.preferences.SetString("TransitRelayAddress", address) -} - -func (s *settings) onVerifyChanged(selected string) { - enabled := selected == "On" - s.preferences.SetBool("Verify", enabled) - if enabled { - s.client.VerifierOk = s.verify - } else { - s.client.VerifierOk = nil - } -} - -func (s *settings) verify(hash string) bool { - verified := make(chan bool) - dialog.ShowCustomConfirm("Verify content", "Accept", "Reject", - container.NewVBox( - newBoldLabel("The hash for your content is:"), - &widget.Label{Text: hash, Wrapping: fyne.TextWrapBreak}, - newBoldLabel("Please verify that the hash is the same on both sides."), - ), func(accept bool) { verified <- accept }, s.window, - ) - - return <-verified -} - -// getPreferences is used to set the preferences on startup without saving at the same time. -func (s *settings) getPreferences(app fyne.App) { - s.client.DownloadPath = s.preferences.StringWithFallback("DownloadPath", util.UserDownloadsFolder()) - s.downloadPathEntry.Text = s.client.DownloadPath - - s.client.OverwriteExisting = s.preferences.Bool("OverwriteFiles") - s.overwriteFiles.Selected = onOrOff(s.client.OverwriteExisting) - - s.client.Notifications = s.preferences.BoolWithFallback("Notifications", true) - s.notificationRadio.Selected = onOrOff(s.client.Notifications) - - s.client.NoExtractDirectory = s.preferences.Bool("NoExtractDirectory") - s.extractRadio.Selected = onOrOff(!s.client.NoExtractDirectory) - - checkUpdates := s.preferences.BoolWithFallback("CheckUpdates", true) - if !updater.Enabled { - checkUpdates = false - s.checkUpdatesRadio.Disable() - } - if checkUpdates { - updater.Enable(app, s.window) - } - s.checkUpdatesRadio.Selected = onOrOff(checkUpdates) - - verify := s.preferences.Bool("Verify") - s.verifyRadio.Selected = onOrOff(verify) - if verify { - s.client.VerifierOk = s.verify - } - - s.client.PassPhraseComponentLength = s.preferences.IntWithFallback("ComponentLength", 2) - s.componentSlider.Value = float64(s.client.PassPhraseComponentLength) - s.componentLabel.Text = string('0' + byte(s.componentSlider.Value)) - - s.client.AppID = s.preferences.String("AppID") - s.appID.Text = s.client.AppID - - s.client.RendezvousURL = s.preferences.String("RendezvousURL") - s.rendezvousURL.Text = s.client.RendezvousURL - - s.client.TransitRelayAddress = s.preferences.String("TransitRelayAddress") - s.transitRelayAddress.Text = s.client.TransitRelayAddress -} - -func (s *settings) buildUI(app fyne.App) *container.Scroll { - onOffOptions := []string{"On", "Off"} - - pathSelector := &widget.Button{Icon: theme.FolderOpenIcon(), Importance: widget.LowImportance, OnTapped: s.onDownloadsPathSelected} - s.downloadPathEntry = &widget.Entry{Scroll: container.ScrollHorizontalOnly, OnSubmitted: s.onDownloadsPathSubmitted, ActionItem: pathSelector} - - s.overwriteFiles = &widget.RadioGroup{Options: onOffOptions, Horizontal: true, Required: true, OnChanged: s.onOverwriteFilesChanged} - - s.notificationRadio = &widget.RadioGroup{Options: onOffOptions, Horizontal: true, Required: true, OnChanged: s.onNotificationsChanged} - - s.extractRadio = &widget.RadioGroup{Options: onOffOptions, Horizontal: true, Required: true, OnChanged: s.onExtractChanged} - - s.checkUpdatesRadio = &widget.RadioGroup{Options: onOffOptions, Horizontal: true, Required: true, OnChanged: s.onCheckUpdatesChanged} - - s.verifyRadio = &widget.RadioGroup{Options: onOffOptions, Horizontal: true, Required: true, OnChanged: s.onVerifyChanged} - - s.componentSlider = &widget.Slider{Min: 2.0, Max: 9.0, Step: 1, OnChanged: s.onComponentsChange, OnChangeEnded: s.onComponentsChangeEnded} - s.componentLabel = &widget.Label{} - - s.appID = widget.NewSelectEntry([]string{wormhole.WormholeCLIAppID}) - s.appID.PlaceHolder = wormhole.WormholeCLIAppID - s.appID.OnChanged = s.onAppIDChanged - - const leastAuthorityRendzvousURL = "wss://mailbox.mw.leastauthority.com/v1" - s.rendezvousURL = widget.NewSelectEntry([]string{wormhole.DefaultRendezvousURL, leastAuthorityRendzvousURL}) - s.rendezvousURL.PlaceHolder = wormhole.DefaultRendezvousURL - s.rendezvousURL.OnChanged = s.onRendezvousURLChange - - const leastAuthorityTransitRelayAddress = "relay.mw.leastauthority.com:4001" - s.transitRelayAddress = widget.NewSelectEntry([]string{wormhole.DefaultTransitRelayAddress, leastAuthorityTransitRelayAddress}) - s.transitRelayAddress.PlaceHolder = wormhole.DefaultTransitRelayAddress - s.transitRelayAddress.OnChanged = s.onTransitAdressChange - - s.getPreferences(app) - - interfaceContainer := appearance.NewSettings().LoadAppearanceScreen(s.window) - - dataContainer := container.NewGridWithColumns(2, - newBoldLabel("Save files to"), s.downloadPathEntry, - newBoldLabel("Overwrite files"), s.overwriteFiles, - newBoldLabel("Notifications"), s.notificationRadio, - newBoldLabel("Extract received directory"), s.extractRadio, - newBoldLabel("Check for updates"), s.checkUpdatesRadio, - ) - - wormholeContainer := container.NewVBox( - container.NewGridWithColumns(2, - newBoldLabel("Verify before accepting"), s.verifyRadio, - newBoldLabel("Passphrase length"), - container.NewBorder(nil, nil, nil, s.componentLabel, s.componentSlider), - ), - &widget.Accordion{Items: []*widget.AccordionItem{ - {Title: "Advanced", Detail: container.NewGridWithColumns(2, - newBoldLabel("AppID"), s.appID, - newBoldLabel("Rendezvous URL"), s.rendezvousURL, - newBoldLabel("Transit Relay Address"), s.transitRelayAddress, - )}, - }}, - ) - - return container.NewScroll(container.NewVBox( - &widget.Card{Title: "User Interface", Content: interfaceContainer}, - &widget.Card{Title: "Data Handling", Content: dataContainer}, - &widget.Card{Title: "Wormhole Options", Content: wormholeContainer}, - )) -} - -func newBoldLabel(text string) *widget.Label { - return &widget.Label{Text: text, TextStyle: fyne.TextStyle{Bold: true}} -} - -func onOrOff(on bool) string { - if on { - return "On" - } - - return "Off" -} diff --git a/internal/ui/setup.go b/internal/ui/setup.go new file mode 100644 index 00000000..f4325ed1 --- /dev/null +++ b/internal/ui/setup.go @@ -0,0 +1,31 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + + "fyne.io/x/fyne/dialog" +) + +// Create sets up the user interface for the application. +func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { + dropdown := &widget.Button{Icon: theme.MenuIcon(), Importance: widget.LowImportance} + dropdown.OnTapped = func() { + widget.ShowPopUpMenuAtRelativePosition( + &fyne.Menu{Items: []*fyne.MenuItem{{Label: "About", Icon: theme.InfoIcon(), Action: func() { + dialog.ShowAboutWindow("Cross-platform application for easy encrypted file, folder, and text sharing between devices.", []*widget.Hyperlink{{Text: "Website"}}, a) + }}}}, w.Canvas(), fyne.Position{Y: dropdown.Size().Height}, dropdown) + } + + tabs := &container.AppTabs{ + Items: []*container.TabItem{ + {Text: "Send", Icon: theme.UploadIcon(), Content: createSendPage(a, w)}, + {Text: "Receive", Icon: theme.DownloadIcon(), Content: createRecvPage(a, w)}, + }, + } + + upperRightCorner := container.NewBorder(container.NewBorder(nil, nil, nil, dropdown), nil, nil, nil) + return container.NewStack(tabs, upperRightCorner) +} diff --git a/internal/ui/tabs.go b/internal/ui/tabs.go deleted file mode 100644 index 20667ca9..00000000 --- a/internal/ui/tabs.go +++ /dev/null @@ -1,90 +0,0 @@ -// Package ui handles all logic related to the user interface. -package ui - -import ( - "os" - "path/filepath" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/storage" - "github.com/Jacalz/rymdport/v3/internal/transport" -) - -// Create will set up and create the ui components. -func Create(app fyne.App, window fyne.Window) *container.AppTabs { - client := transport.NewClient(app) - - send := newSend(window, client) - tabs := &container.AppTabs{ - Items: []*container.TabItem{ - send.newSendTab(), - newRecvTab(window, client), - newSettingsTab(window, client), - newAboutTab(app), - }, - } - - window.SetOnDropped(func(_ fyne.Position, uris []fyne.URI) { - if tabs.SelectedIndex() != 0 { - tabs.SelectIndex(0) - } - - dialog.ShowConfirm("Custom Code", "Use a custom code?", func(custom bool) { - send.client.CustomCode = custom - send.newTransfer(uris) - }, window) - }) - - if args := os.Args[1:]; len(args) > 0 { - uris := make([]fyne.URI, 0, len(args)) - for _, path := range args { - path, err := filepath.Abs(path) - if err != nil { - fyne.LogError("Failed to create absolute path", err) - continue - } - - uris = append(uris, storage.NewFileURI(path)) - } - - send.newTransfer(uris) - } - - canvas := window.Canvas() - - // Set up support for switching between the tabs. - ctrlTab := &desktop.CustomShortcut{KeyName: fyne.KeyTab, Modifier: fyne.KeyModifierControl} - canvas.AddShortcut(ctrlTab, func(_ fyne.Shortcut) { - next := tabs.SelectedIndex() + 1 - if next >= len(tabs.Items) { - next = 0 - } - - tabs.SelectIndex(next) - }) - - ctrlShiftTab := &desktop.CustomShortcut{KeyName: fyne.KeyTab, Modifier: fyne.KeyModifierControl | fyne.KeyModifierShift} - canvas.AddShortcut(ctrlShiftTab, func(_ fyne.Shortcut) { - next := tabs.SelectedIndex() - 1 - if next < 0 { - next += len(tabs.Items) - } - - tabs.SelectIndex(next) - }) - - // Set up support for Alt + [1:4] for switching to a specific tab. - alt1 := &desktop.CustomShortcut{KeyName: fyne.Key1, Modifier: fyne.KeyModifierAlt} - canvas.AddShortcut(alt1, func(_ fyne.Shortcut) { tabs.SelectIndex(0) }) - alt2 := &desktop.CustomShortcut{KeyName: fyne.Key2, Modifier: fyne.KeyModifierAlt} - canvas.AddShortcut(alt2, func(_ fyne.Shortcut) { tabs.SelectIndex(1) }) - alt3 := &desktop.CustomShortcut{KeyName: fyne.Key3, Modifier: fyne.KeyModifierAlt} - canvas.AddShortcut(alt3, func(_ fyne.Shortcut) { tabs.SelectIndex(2) }) - alt4 := &desktop.CustomShortcut{KeyName: fyne.Key4, Modifier: fyne.KeyModifierAlt} - canvas.AddShortcut(alt4, func(_ fyne.Shortcut) { tabs.SelectIndex(3) }) - - return tabs -} diff --git a/internal/ui/tabs_test.go b/internal/ui/tabs_test.go deleted file mode 100644 index 894c8d03..00000000 --- a/internal/ui/tabs_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package ui - -import ( - "os" - "testing" - - "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/container" -) - -var globalTabs *container.AppTabs - -func BenchmarkCreate(b *testing.B) { - b.StopTimer() - a := app.NewWithID("io.github.jacalz.rymdport") - w := a.NewWindow("Rymdport") - var tabs *container.AppTabs - os.Args = []string{"rymdport"} // Don't read test arguments as uri input. - b.StartTimer() - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - tabs = Create(a, w) - } - - // Don't allow the compiler to optimize out. - globalTabs = tabs -} From d35cffe05f3c48b2f6c5ac6fde8f54579f41bae5 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:02:57 +0100 Subject: [PATCH 02/16] Mockup send tab --- internal/ui/send.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/ui/send.go b/internal/ui/send.go index 8b6b0b0a..753a6067 100644 --- a/internal/ui/send.go +++ b/internal/ui/send.go @@ -2,9 +2,25 @@ package ui import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) func createSendPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { - return widget.NewLabel("Send something") + icon := canvas.NewImageFromResource(theme.UploadIcon()) + icon.SetMinSize(fyne.NewSquareSize(200)) + + title := &widget.Label{Text: "Send Data", Alignment: fyne.TextAlignCenter, TextStyle: fyne.TextStyle{Bold: true}} + description := &widget.Label{Text: "Select data type below or drop files here", Alignment: fyne.TextAlignCenter} + + // Buttons for starting sends. + file := &widget.Button{Icon: theme.FileTextIcon(), Text: "Send File"} + folder := &widget.Button{Icon: theme.FolderIcon(), Text: "Send Folder"} + text := &widget.Button{Icon: theme.DocumentIcon(), Text: "Send Text"} + + buttons := container.NewVBox(icon, &widget.Separator{}, title, description, &widget.Separator{}, file, folder, text) + + return container.NewCenter(buttons) } From c39fe7a0eee1b7071b44f53fe51f6438919f7f08 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:08:24 +0100 Subject: [PATCH 03/16] Simple mockup for new receive tab --- internal/ui/recv.go | 17 ++++++++++++++++- internal/ui/send.go | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/ui/recv.go b/internal/ui/recv.go index afa00629..494925ad 100644 --- a/internal/ui/recv.go +++ b/internal/ui/recv.go @@ -2,9 +2,24 @@ package ui import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/Jacalz/rymdport/v3/internal/util" ) func createRecvPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { - return widget.NewLabel("Receive something") + icon := canvas.NewImageFromResource(theme.DownloadIcon()) + icon.FillMode = canvas.ImageFillContain + icon.SetMinSize(fyne.NewSquareSize(200)) + + title := &widget.Label{Text: "Receive Data", Alignment: fyne.TextAlignCenter, TextStyle: fyne.TextStyle{Bold: true}} + description := &widget.Label{Text: "Enter a code below to start receiving data", Alignment: fyne.TextAlignCenter} + + code := &widget.Entry{PlaceHolder: "Code from sender", Validator: util.CodeValidator} + + content := container.NewVBox(icon, &widget.Separator{}, title, description, &widget.Separator{}, code) + + return container.NewCenter(content) } diff --git a/internal/ui/send.go b/internal/ui/send.go index 753a6067..77ee470c 100644 --- a/internal/ui/send.go +++ b/internal/ui/send.go @@ -10,6 +10,7 @@ import ( func createSendPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { icon := canvas.NewImageFromResource(theme.UploadIcon()) + icon.FillMode = canvas.ImageFillContain icon.SetMinSize(fyne.NewSquareSize(200)) title := &widget.Label{Text: "Send Data", Alignment: fyne.TextAlignCenter, TextStyle: fyne.TextStyle{Bold: true}} From 42762c3fec3babab87fc79fa00e0ddfb561634e1 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:18:43 +0100 Subject: [PATCH 04/16] Improve the about window contents and size --- internal/ui/setup.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/ui/setup.go b/internal/ui/setup.go index f4325ed1..e3149502 100644 --- a/internal/ui/setup.go +++ b/internal/ui/setup.go @@ -5,6 +5,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/Jacalz/rymdport/v3/internal/util" "fyne.io/x/fyne/dialog" ) @@ -15,7 +16,14 @@ func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { dropdown.OnTapped = func() { widget.ShowPopUpMenuAtRelativePosition( &fyne.Menu{Items: []*fyne.MenuItem{{Label: "About", Icon: theme.InfoIcon(), Action: func() { - dialog.ShowAboutWindow("Cross-platform application for easy encrypted file, folder, and text sharing between devices.", []*widget.Hyperlink{{Text: "Website"}}, a) + links := []*widget.Hyperlink{ + {Text: "Repository", URL: util.URLToGitHubProject("")}, + {Text: "Issue Tracker", URL: util.URLToGitHubProject("/issues")}, + {Text: "Wiki", URL: util.URLToGitHubProject("/wiki")}, + } + abwin := dialog.NewAboutWindow("Easy encrypted file, folder, and text sharing between devices.", links, a) + abwin.Resize(fyne.NewSize(500, 300)) + abwin.Show() }}}}, w.Canvas(), fyne.Position{Y: dropdown.Size().Height}, dropdown) } From 06c6d4f0ac52c694b568b25fb51ad9646aef84c2 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:27:40 +0100 Subject: [PATCH 05/16] Improve ui styling a bit --- internal/ui/recv.go | 6 +++--- internal/ui/send.go | 16 +++++++--------- main.go | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/internal/ui/recv.go b/internal/ui/recv.go index 494925ad..c6b21940 100644 --- a/internal/ui/recv.go +++ b/internal/ui/recv.go @@ -14,12 +14,12 @@ func createRecvPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { icon.FillMode = canvas.ImageFillContain icon.SetMinSize(fyne.NewSquareSize(200)) - title := &widget.Label{Text: "Receive Data", Alignment: fyne.TextAlignCenter, TextStyle: fyne.TextStyle{Bold: true}} - description := &widget.Label{Text: "Enter a code below to start receiving data", Alignment: fyne.TextAlignCenter} + description := &widget.Label{Text: "Enter a code below to start receiving data.", Alignment: fyne.TextAlignCenter} code := &widget.Entry{PlaceHolder: "Code from sender", Validator: util.CodeValidator} + start := &widget.Button{Text: "Start Receive", Icon: theme.DownloadIcon(), Importance: widget.HighImportance} - content := container.NewVBox(icon, &widget.Separator{}, title, description, &widget.Separator{}, code) + content := container.NewVBox(icon, description, &widget.Separator{}, code, &widget.Separator{}, container.NewCenter(start)) return container.NewCenter(content) } diff --git a/internal/ui/send.go b/internal/ui/send.go index 77ee470c..b2795b8e 100644 --- a/internal/ui/send.go +++ b/internal/ui/send.go @@ -13,15 +13,13 @@ func createSendPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { icon.FillMode = canvas.ImageFillContain icon.SetMinSize(fyne.NewSquareSize(200)) - title := &widget.Label{Text: "Send Data", Alignment: fyne.TextAlignCenter, TextStyle: fyne.TextStyle{Bold: true}} - description := &widget.Label{Text: "Select data type below or drop files here", Alignment: fyne.TextAlignCenter} + description := &widget.Label{Text: "Select data type below or drop files here.", Alignment: fyne.TextAlignCenter} - // Buttons for starting sends. - file := &widget.Button{Icon: theme.FileTextIcon(), Text: "Send File"} - folder := &widget.Button{Icon: theme.FolderIcon(), Text: "Send Folder"} - text := &widget.Button{Icon: theme.DocumentIcon(), Text: "Send Text"} + file := &widget.Button{Icon: theme.FileTextIcon(), Text: "Send File", Importance: widget.HighImportance} + folder := &widget.Button{Icon: theme.FolderIcon(), Text: "Send Folder", Importance: widget.HighImportance} + text := &widget.Button{Icon: theme.DocumentIcon(), Text: "Send Text", Importance: widget.HighImportance} - buttons := container.NewVBox(icon, &widget.Separator{}, title, description, &widget.Separator{}, file, folder, text) - - return container.NewCenter(buttons) + buttons := container.NewCenter(container.NewHBox(file, &widget.Separator{}, folder, &widget.Separator{}, text)) + content := container.NewVBox(icon, description, &widget.Separator{}, buttons) + return container.NewCenter(content) } diff --git a/main.go b/main.go index b8d90efc..32f206e6 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ func main() { w := a.NewWindow("Rymdport") w.SetContent(ui.Create(a, w)) - w.Resize(fyne.NewSize(700, 400)) + w.Resize(fyne.NewSize(600, 400)) w.SetMaster() w.ShowAndRun() } From cc4f352ed117f50d45b703633a2ac990a7be7197 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:53:56 +0100 Subject: [PATCH 06/16] Rip out the bridge package --- internal/transport/bridge/bridge.go | 49 --- internal/transport/bridge/layout.go | 44 --- internal/transport/bridge/recv.go | 322 -------------------- internal/transport/bridge/send.go | 447 ---------------------------- 4 files changed, 862 deletions(-) delete mode 100644 internal/transport/bridge/bridge.go delete mode 100644 internal/transport/bridge/layout.go delete mode 100644 internal/transport/bridge/recv.go delete mode 100644 internal/transport/bridge/send.go diff --git a/internal/transport/bridge/bridge.go b/internal/transport/bridge/bridge.go deleted file mode 100644 index 40223e2d..00000000 --- a/internal/transport/bridge/bridge.go +++ /dev/null @@ -1,49 +0,0 @@ -// Package bridge serves as a bridge between the transport backend and the Fyne ui -package bridge - -import ( - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" -) - -func newCodeDisplay(window fyne.Window) *fyne.Container { - codeLabel := &widget.Label{Text: "Waiting for code...", Truncation: fyne.TextTruncateEllipsis} - copyButton := &widget.Button{Icon: theme.ContentCopyIcon(), Importance: widget.LowImportance} - - copyButton.OnTapped = func() { - if codeLabel.Text != "Waiting for code..." { - copyButton.SetIcon(theme.ConfirmIcon()) - window.Clipboard().SetContent(codeLabel.Text) - } else { - copyButton.SetIcon(theme.CancelIcon()) - } - - time.Sleep(500 * time.Millisecond) - copyButton.SetIcon(theme.ContentCopyIcon()) - } - - return container.New(codeLayout{}, codeLabel, copyButton) -} - -type codeLayout struct{} - -func (c codeLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { - displacement := size.Width * 0.8 - - objects[0].Move(fyne.NewSquareOffsetPos(0)) - objects[0].Resize(fyne.NewSize(displacement, size.Height)) - - objects[1].Move(fyne.NewPos(displacement, 0)) - objects[1].Resize(fyne.NewSquareSize(size.Height)) -} - -func (c codeLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { - leftMin := objects[0].MinSize() - rightMin := objects[1].MinSize() - - return fyne.NewSize(leftMin.Width+leftMin.Width, fyne.Max(leftMin.Height, rightMin.Height)) -} diff --git a/internal/transport/bridge/layout.go b/internal/transport/bridge/layout.go deleted file mode 100644 index 6a3247eb..00000000 --- a/internal/transport/bridge/layout.go +++ /dev/null @@ -1,44 +0,0 @@ -package bridge - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" -) - -type listLayout struct{} - -func (g listLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { - padding := theme.InnerPadding() - doublePadding := 2 * padding - - objects[0].Move(fyne.NewPos(0, padding)) - objects[0].Resize(fyne.NewSize(size.Height-padding, size.Height-doublePadding)) - - cellSize := (size.Width - size.Height - doublePadding) / (float32(len(objects) - 1)) - start, end := size.Height, size.Height+cellSize-padding - for _, child := range objects[1:] { - child.Move(fyne.NewPos(start, padding)) - child.Resize(fyne.NewSize(end-start, size.Height-doublePadding)) - - start = end + padding - end = start + cellSize - } -} - -// MinSize finds the smallest size that satisfies all the child objects. -// Height will stay consistent between each each instance. -func (g listLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { - doublePadding := 2 * theme.InnerPadding() - - maxMinSizeWidth := float32(0) - maxMinSizeHeight := theme.IconInlineSize() + doublePadding // Default button height with icon - for _, child := range objects { - if child.Visible() { - min := child.MinSize() - maxMinSizeWidth += min.Width - maxMinSizeHeight = fyne.Max(min.Height, maxMinSizeHeight) - } - } - - return fyne.NewSize(maxMinSizeWidth, maxMinSizeHeight+doublePadding) -} diff --git a/internal/transport/bridge/recv.go b/internal/transport/bridge/recv.go deleted file mode 100644 index 2aa10ff8..00000000 --- a/internal/transport/bridge/recv.go +++ /dev/null @@ -1,322 +0,0 @@ -package bridge - -import ( - "path/filepath" - "slices" - "sync" - "sync/atomic" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/transport" - "github.com/Jacalz/rymdport/v3/internal/util" - "github.com/rymdport/wormhole/wormhole" -) - -// RecvItem is the item that is being received -type RecvItem struct { - URI fyne.URI - Code string - - Value int64 - Max int64 - Status func() string - - // Allow the list to only refresh a single object. - refresh func(int) - index int -} - -func (r *RecvItem) update(delta, total int64) { - r.Value += delta - r.Max = total - r.refresh(r.index) -} - -func (r *RecvItem) done() { - r.Value = r.Max - r.refresh(r.index) -} - -func (r *RecvItem) failed() { - r.Status = func() string { return "Failed" } - r.refresh(r.index) -} - -// RecvData is a list of progress bars that track send progress. -type RecvData struct { - Client *transport.Client - Window fyne.Window - - items []*RecvItem - info recvInfoDialog - textWindow textRecvWindow - - deleting atomic.Bool - lock sync.Mutex - list *widget.List -} - -// NewRecvList greates a list of progress bars. -func (d *RecvData) NewRecvList() *widget.List { - d.list = &widget.List{ - Length: d.Length, - CreateItem: d.CreateItem, - UpdateItem: d.UpdateItem, - OnSelected: d.OnSelected, - } - d.setUpInfoDialog() - return d.list -} - -// Length returns the length of the data. -func (d *RecvData) Length() int { - return len(d.items) -} - -// CreateItem creates a new item in the list. -func (d *RecvData) CreateItem() fyne.CanvasObject { - return container.New(listLayout{}, - &widget.FileIcon{}, - &widget.Label{Text: "Waiting for filename...", Truncation: fyne.TextTruncateEllipsis}, - &widget.Label{Text: "Waiting for code...", Truncation: fyne.TextTruncateEllipsis}, - &widget.ProgressBar{}, - ) -} - -// UpdateItem updates the data in the list. -func (d *RecvData) UpdateItem(i int, object fyne.CanvasObject) { - item := d.items[i] - container := object.(*fyne.Container) - container.Objects[0].(*widget.FileIcon).SetURI(item.URI) - container.Objects[1].(*widget.Label).SetText(item.URI.Name()) - container.Objects[2].(*widget.Label).SetText(item.Code) - - progress := container.Objects[3].(*widget.ProgressBar) - progress.Max = float64(item.Max) - progress.Value = float64(item.Value) - progress.TextFormatter = item.Status - progress.Refresh() -} - -// OnSelected currently just makes sure that we don't persist selection. -func (d *RecvData) OnSelected(i int) { - d.list.Unselect(i) - - d.info.button.OnTapped = func() { - d.remove(i) - d.info.dialog.Hide() - } - - if d.info.button.Disabled() { - d.info.label.Text = "This item can be removed.\nThe transfer has completed." - d.info.button.Enable() - } - - // Only allow failed or completed items to be removed. - item := d.items[i] - if item.Value < item.Max && item.Status == nil { - d.info.label.Text = "This item can't be removed yet.\nThe transfer needs to complete first." - d.info.button.Disable() - } - - d.info.dialog.Show() -} - -func (d *RecvData) NewFailedRecv(code string) { - d.lock.Lock() - item := &RecvItem{URI: storage.NewFileURI("Failed"), Code: code, Max: 1, refresh: d.refresh, index: len(d.items)} - item.Status = func() string { return "failed" } - d.items = append(d.items, item) - d.lock.Unlock() - - d.list.Refresh() -} - -// NewRecv creates a new item to be displayed and returns it plus the path to the file. -// A text receive will have a path of an empty string. -func (d *RecvData) NewRecv(code string, msg *wormhole.IncomingMessage) (item *RecvItem, path string) { - d.lock.Lock() - item = &RecvItem{Code: code, Max: 1, refresh: d.refresh, index: len(d.items)} - - if msg.Type == wormhole.TransferText { - item.URI = storage.NewFileURI("Text Snippet") - } else { - path = filepath.Join(d.Client.DownloadPath, msg.Name) - item.URI = storage.NewFileURI(path) - - if msg.Type == wormhole.TransferDirectory { - item.URI = &folderURI{item.URI} - } - } - - d.items = append(d.items, item) - d.lock.Unlock() - - d.list.Refresh() - return item, path -} - -// NewReceive adds data about a new send to the list and then returns the channel to update the code. -func (d *RecvData) NewReceive(code string) { - go func(code string) { - msg, err := d.Client.NewReceive(code) - if err != nil { - d.Client.ShowNotification("Receive failed", "An error occurred when receiving the data.") - d.NewFailedRecv(code) - dialog.ShowError(err, d.Window) - return - } - - item, path := d.NewRecv(code, msg) - - if msg.Type == wormhole.TransferText { - d.showTextWindow(msg.ReadText()) - item.done() - - d.Client.ShowNotification("Receive completed", "The text was received successfully.") - return - } - - err = d.Client.SaveToDisk(msg, path, item.update) - if err != nil { - item.failed() - d.Client.ShowNotification("Receive failed", "An error occurred when receiving the data.") - dialog.ShowError(err, d.Window) - return - } - - d.Client.ShowNotification("Receive completed", "The contents were saved to "+filepath.Dir(item.URI.Path())+".") - item.done() - }(code) -} - -func (d *RecvData) refresh(index int) { - if d.deleting.Load() { - return // Don't update if we are deleting. - } - - d.list.RefreshItem(index) -} - -func (d *RecvData) remove(index int) { - // Make sure that no updates happen while we modify the slice. - d.deleting.Store(true) - - d.items[index] = nil // TODO: Remove once we have Go 1.22 as the base. - d.items = slices.Delete(d.items, index, index+1) - - // Update the moved items to have the correct index. - for j := index; j < len(d.items); j++ { - d.items[j].index = j - } - - // Refresh the whole list. - d.list.Refresh() - - // Allow individual objects to be refreshed again. - d.deleting.Store(false) -} - -func (d *RecvData) setUpInfoDialog() { - d.info.label = &widget.Label{Text: "This item can be removed.\nThe transfer has completed."} - d.info.button = &widget.Button{Icon: theme.DeleteIcon(), Importance: widget.DangerImportance, Text: "Remove"} - removeCard := &widget.Card{Content: container.NewVBox(d.info.label, d.info.button)} - d.info.dialog = dialog.NewCustom("Information", "Close", removeCard, d.Window) -} - -type recvInfoDialog struct { - dialog *dialog.CustomDialog - button *widget.Button - label *widget.Label -} - -type textRecvWindow struct { - textLabel *widget.Label - copyButton, saveButton *widget.Button - window fyne.Window - fileSaveDialog *dialog.FileDialog -} - -func (r *textRecvWindow) copy() { - r.window.Clipboard().SetContent(r.textLabel.Text) -} - -func (r *textRecvWindow) interceptClose() { - r.window.Hide() - r.textLabel.SetText("") -} - -func (r *textRecvWindow) saveFileToDisk(file fyne.URIWriteCloser, err error) { - if err != nil { - fyne.LogError("Error on selecting file to write to", err) - dialog.ShowError(err, r.window) - return - } else if file == nil { - return - } - - if _, err := file.Write([]byte(r.textLabel.Text)); err != nil { - fyne.LogError("Error on writing text to the file", err) - dialog.ShowError(err, r.window) - } - - if err := file.Close(); err != nil { - fyne.LogError("Error on closing text file", err) - dialog.ShowError(err, r.window) - } -} - -func (r *textRecvWindow) save() { - now := time.Now().Format("2006-01-02T15:04") // TODO: Might want to use AppendFormat and strings.Builder - r.fileSaveDialog.SetFileName("received-" + now + ".txt") - r.fileSaveDialog.Resize(util.WindowSizeToDialog(r.window.Canvas().Size())) - r.fileSaveDialog.Show() -} - -func (d *RecvData) createTextWindow() { - window := d.Client.App.NewWindow("Received Text") - window.SetCloseIntercept(d.textWindow.interceptClose) - - d.textWindow = textRecvWindow{ - window: window, - textLabel: &widget.Label{}, - copyButton: &widget.Button{Text: "Copy", Icon: theme.ContentCopyIcon(), OnTapped: d.textWindow.copy}, - saveButton: &widget.Button{Text: "Save", Icon: theme.DocumentSaveIcon(), OnTapped: d.textWindow.save}, - fileSaveDialog: dialog.NewFileSave(d.textWindow.saveFileToDisk, window), - } - - actionContainer := container.NewGridWithColumns(2, d.textWindow.copyButton, d.textWindow.saveButton) - window.SetContent(container.NewBorder(nil, actionContainer, nil, nil, container.NewScroll(d.textWindow.textLabel))) - window.Resize(fyne.NewSize(400, 300)) -} - -// showTextWindow handles the creation of a window for displaying text content. -func (d *RecvData) showTextWindow(received string) { - if d.textWindow.window == nil { - d.createTextWindow() - } - - d.textWindow.textLabel.SetText(received) - - win := d.textWindow.window - win.Show() - win.RequestFocus() -} - -var _ fyne.URIWithIcon = (*folderURI)(nil) - -// folderURI is used to force folders to use an icon even before the actual folder is created. -// This avoids issues with the icon initially appearing as a file. -type folderURI struct { - fyne.URI -} - -func (c *folderURI) Icon() fyne.Resource { - return theme.FolderIcon() -} diff --git a/internal/transport/bridge/send.go b/internal/transport/bridge/send.go deleted file mode 100644 index bce1bcfc..00000000 --- a/internal/transport/bridge/send.go +++ /dev/null @@ -1,447 +0,0 @@ -package bridge - -import ( - "path/filepath" - "slices" - "sync/atomic" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/transport" - "github.com/Jacalz/rymdport/v3/internal/util" - "github.com/rymdport/go-qrcode" - "github.com/rymdport/wormhole/wormhole" -) - -// SendItem is the item that is being sent. -type SendItem struct { - URI fyne.URI - Code string - - Value int64 - Max int64 - Status func() string - - // Allow the list to only refresh a single object. - refresh func(int) - index int -} - -func (s *SendItem) update(sent, total int64) { - s.Value = sent - s.Max = total - s.refresh(s.index) -} - -func (s *SendItem) failed() { - s.Status = func() string { return "Failed" } - s.refresh(s.index) -} - -// SendData is a list of progress bars that track send progress. -type SendData struct { - Client *transport.Client - Window fyne.Window - - items []*SendItem - info sendInfoDialog - textWindow textSendWindow - - deleting atomic.Bool - list *widget.List -} - -// NewSendList greates a list of progress bars. -func (d *SendData) NewSendList() *widget.List { - d.list = &widget.List{ - Length: d.Length, - CreateItem: d.CreateItem, - UpdateItem: d.UpdateItem, - OnSelected: d.OnSelected, - } - d.setUpInfoDialog() - return d.list -} - -// Length returns the length of the data. -func (d *SendData) Length() int { - return len(d.items) -} - -// CreateItem creates a new item in the list. -func (d *SendData) CreateItem() fyne.CanvasObject { - return container.New(listLayout{}, - &widget.FileIcon{}, - &widget.Label{Text: "Waiting for filename...", Truncation: fyne.TextTruncateEllipsis}, - newCodeDisplay(d.Window), - &widget.ProgressBar{}, - ) -} - -// UpdateItem updates the data in the list. -func (d *SendData) UpdateItem(i int, object fyne.CanvasObject) { - container := object.(*fyne.Container) - - item := d.items[i] - container.Objects[0].(*widget.FileIcon).SetURI(item.URI) - container.Objects[1].(*widget.Label).SetText(item.URI.Name()) - container.Objects[2].(*fyne.Container).Objects[0].(*widget.Label).SetText(item.Code) - - progress := container.Objects[3].(*widget.ProgressBar) - progress.Max = float64(item.Max) - progress.Value = float64(item.Value) - progress.TextFormatter = item.Status - progress.Refresh() -} - -// OnSelected currently just makes sure that we don't persist selection. -func (d *SendData) OnSelected(i int) { - d.list.Unselect(i) - - code, err := qrcode.New("wormhole-transfer:"+d.items[i].Code, qrcode.High) - if err != nil { - fyne.LogError("Failed to encode qr code", err) - return - } - - code.BackgroundColor = theme.Color(theme.ColorNameOverlayBackground) - code.ForegroundColor = theme.Color(theme.ColorNameForeground) - d.info.image.Image = code.Image(100) - d.info.image.Resource = nil - d.info.image.ScaleMode = canvas.ImageScalePixels - d.info.image.Refresh() - - d.info.button.OnTapped = func() { - d.remove(i) - d.info.dialog.Hide() - } - - if d.info.button.Disabled() { - d.info.label.Text = "This item can be removed.\nThe transfer has completed." - d.info.button.Enable() - } - - // Only allow failed or completed items to be removed. - item := d.items[i] - if item.Value < item.Max && item.Status == nil { - d.info.label.Text = "This item can't be removed yet.\nThe transfer needs to complete first." - d.info.button.Disable() - } else { - d.info.image.Image = nil - d.info.image.Resource = theme.BrokenImageIcon() - d.info.image.ScaleMode = canvas.ImageScaleSmooth - d.info.image.Refresh() - - // TODO: Display something like: "This transfer is not active.\nCan't show a QR code.". - } - - d.info.dialog.Show() -} - -// NewSend adds data about a new send to the list and then returns the item. -func (d *SendData) NewSend(uri fyne.URI) *SendItem { - item := &SendItem{Code: "Waiting for code...", URI: uri, Max: 1, refresh: d.refresh, index: len(d.items)} - d.items = append(d.items, item) - return item -} - -// OnFileSelect is intended to be passed as callback to a FileOpen dialog. -func (d *SendData) OnFileSelect(file fyne.URIReadCloser, err error) { - if err != nil { - fyne.LogError("Error on selecting file to send", err) - dialog.ShowError(err, d.Window) - return - } else if file == nil { - return - } - - item := d.NewSend(file.URI()) - d.list.Refresh() - - go func() { - // We want to catch close errors for security reasons. - defer func() { - if err = file.Close(); err != nil { - item.failed() - fyne.LogError("Error on closing file", err) - } - }() - - code, result, err := d.Client.NewFileSend(file, wormhole.WithProgress(item.update), d.getCustomCode()) - if err != nil { - fyne.LogError("Error on sending file", err) - item.failed() - dialog.ShowError(err, d.Window) - return - } - - item.Code = code - d.refresh(item.index) - - if res := <-result; res.Error != nil { - fyne.LogError("Error on sending file", res.Error) - item.failed() - dialog.ShowError(res.Error, d.Window) - d.Client.ShowNotification("File send failed", "An error occurred when sending the file.") - } else if res.OK { - d.Client.ShowNotification("File send completed", "The file was sent successfully.") - } - }() -} - -// OnDirSelect is intended to be passed as callback to a FolderOpen dialog. -func (d *SendData) OnDirSelect(dir fyne.ListableURI, err error) { - if err != nil { - fyne.LogError("Error on selecting dir to send", err) - dialog.ShowError(err, d.Window) - return - } else if dir == nil { - return - } - - item := d.NewSend(dir) - d.list.Refresh() - - go func() { - code, result, err := d.Client.NewDirSend(dir, wormhole.WithProgress(item.update), d.getCustomCode()) - if err != nil { - fyne.LogError("Error on sending directory", err) - item.failed() - dialog.ShowError(err, d.Window) - return - } - - item.Code = code - d.refresh(item.index) - - if res := <-result; res.Error != nil { - fyne.LogError("Error on sending directory", res.Error) - item.failed() - dialog.ShowError(res.Error, d.Window) - d.Client.ShowNotification("Directory send failed", "An error occurred when sending the directory.") - } else if res.OK { - d.Client.ShowNotification("Directory send completed", "The directory was sent successfully.") - } - }() -} - -// NewSendFromFiles creates a directory from the files and sends it as a directory send. -func (d *SendData) NewSendFromFiles(uris []fyne.URI) { - parentDir := storage.NewFileURI(filepath.Dir(uris[0].Path())) - item := d.NewSend(parentDir) - d.list.Refresh() - - go func() { - code, result, err := d.Client.NewMultipleFileSend(uris, wormhole.WithProgress(item.update), d.getCustomCode()) - if err != nil { - fyne.LogError("Error on sending directory", err) - item.failed() - dialog.ShowError(err, d.Window) - return - } - - item.Code = code - d.refresh(item.index) - - if res := <-result; res.Error != nil { - fyne.LogError("Error on sending directory", res.Error) - item.failed() - dialog.ShowError(res.Error, d.Window) - d.Client.ShowNotification("Directory send failed", "An error occurred when sending the directory.") - } else if res.OK { - d.Client.ShowNotification("Directory send completed", "The directory was sent successfully.") - } - }() -} - -// SendText sends new text. -func (d *SendData) SendText() { - go func() { - text := d.showTextWindow() - if text == "" { - return - } - - d.Window.RequestFocus() // Refocus the main window - item := d.NewSend(storage.NewFileURI("Text Snippet")) - d.list.Refresh() - - code, result, err := d.Client.NewTextSend(text, wormhole.WithProgress(item.update), d.getCustomCode()) - if err != nil { - fyne.LogError("Error on sending text", err) - item.failed() - dialog.ShowError(err, d.Window) - return - } - - item.Code = code - d.refresh(item.index) - - if res := <-result; res.Error != nil { - fyne.LogError("Error on sending text", res.Error) - item.failed() - dialog.ShowError(res.Error, d.Window) - d.Client.ShowNotification("Text send failed", "An error occurred when sending the text.") - } else if res.OK && d.Client.Notifications { - d.Client.ShowNotification("Text send completed", "The text was sent successfully.") - } - }() -} - -// getCustomCode returns "" if the user has custom codes disabled. -// Otherwise, it will ask the user for a code. -func (d *SendData) getCustomCode() string { - if !d.Client.CustomCode { - return "" - } - - code := make(chan string) - codeEntry := &widget.Entry{ - PlaceHolder: "123-example-code", - Scroll: container.ScrollBoth, - Validator: util.CodeValidator, - } - - form := dialog.NewForm("Create custom code", "Confirm", "Cancel", []*widget.FormItem{ - { - Text: "Code", Widget: codeEntry, - HintText: "A code beginning with a number, followed by groups of letters separated with \"-\".", - }, - }, func(submitted bool) { - if !submitted || codeEntry.Text == codeEntry.PlaceHolder { - code <- "" - } else { - code <- codeEntry.Text - } - - close(code) - }, d.Window) - form.Resize(fyne.Size{Width: d.Window.Canvas().Size().Width * 0.8}) - codeEntry.OnSubmitted = func(_ string) { form.Submit() } - form.Show() - d.Window.Canvas().Focus(codeEntry) - - return <-code -} - -func (d *SendData) refresh(index int) { - if d.deleting.Load() { - return // Don't update if we are deleting. - } - - d.list.RefreshItem(index) -} - -func (d *SendData) remove(index int) { - // Make sure that no updates happen while we modify the slice. - d.deleting.Store(true) - - d.items[index] = nil // TODO: Remove once we have Go 1.22 as the base. - d.items = slices.Delete(d.items, index, index+1) - - // Update the moved items to have the correct index. - for j := index; j < len(d.items); j++ { - d.items[j].index = j - } - - // Refresh the whole list. - d.list.Refresh() - - // Allow individual objects to be refreshed again. - d.deleting.Store(false) -} - -func (d *SendData) setUpInfoDialog() { - d.info.label = &widget.Label{Text: "This item can be removed.\nThe transfer has completed."} - d.info.button = &widget.Button{Icon: theme.DeleteIcon(), Importance: widget.DangerImportance, Text: "Remove"} - - image := &canvas.Image{} - image.FillMode = canvas.ImageFillOriginal - image.ScaleMode = canvas.ImageScalePixels - image.SetMinSize(fyne.NewSize(100, 100)) - d.info.image = image - - supportedClientsURL := util.URLToGitHubProject("/wiki/Supported-clients") - qrCodeInfo := widget.NewRichText(&widget.TextSegment{ - Style: widget.RichTextStyleInline, - Text: "A list of supported apps can be found ", - }, &widget.HyperlinkSegment{ - Text: "here", - URL: supportedClientsURL, - }, &widget.TextSegment{ - Style: widget.RichTextStyleInline, - Text: ".", - }) - qrCard := &widget.Card{Image: image, Content: container.NewCenter(qrCodeInfo)} - - removeCard := &widget.Card{Content: container.NewVBox(d.info.label, d.info.button)} - - content := container.NewGridWithColumns(2, qrCard, removeCard) - d.info.dialog = dialog.NewCustom("Information", "Close", content, d.Window) -} - -type sendInfoDialog struct { - dialog *dialog.CustomDialog - button *widget.Button - label *widget.Label - image *canvas.Image -} - -type textSendWindow struct { - textEntry *widget.Entry - cancelButton, sendButton *widget.Button - window fyne.Window - text chan string -} - -func (s *textSendWindow) dismiss() { - s.text <- "" - s.window.Hide() - s.textEntry.SetText("") -} - -func (s *textSendWindow) send() { - s.text <- s.textEntry.Text - s.window.Hide() - s.textEntry.SetText("") -} - -func (d *SendData) createTextWindow() { - window := d.Client.App.NewWindow("Send Text") - window.SetCloseIntercept(d.textWindow.dismiss) - - d.textWindow = textSendWindow{ - window: window, - textEntry: &widget.Entry{MultiLine: true, Wrapping: fyne.TextWrapWord, OnSubmitted: func(_ string) { d.textWindow.send() }}, - cancelButton: &widget.Button{Text: "Cancel", Icon: theme.CancelIcon(), OnTapped: d.textWindow.dismiss}, - sendButton: &widget.Button{Text: "Send", Icon: theme.MailSendIcon(), Importance: widget.HighImportance, OnTapped: d.textWindow.send}, - text: make(chan string), - } - - actionContainer := container.NewGridWithColumns(2, d.textWindow.cancelButton, d.textWindow.sendButton) - window.SetContent(container.NewBorder(nil, actionContainer, nil, nil, d.textWindow.textEntry)) - window.Resize(fyne.NewSize(400, 300)) -} - -// showTextWindow opens a new window for setting up text to send. -func (d *SendData) showTextWindow() string { - if d.textWindow.window == nil { - d.createTextWindow() - } else if d.textWindow.window.Canvas().Content().Visible() { - d.textWindow.window.RequestFocus() - return "" - } - - win := d.textWindow.window - - win.Show() - win.RequestFocus() - win.Canvas().Focus(d.textWindow.textEntry) - - return <-d.textWindow.text -} From 741b7cceb8bb9d175087263e99ac6da18efc8268 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 12:56:07 +0100 Subject: [PATCH 07/16] Remove QR code for now --- go.mod | 1 - go.sum | 2 -- internal/ui/components/stack.go | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 internal/ui/components/stack.go diff --git a/go.mod b/go.mod index 4cff2254..0d55f88c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/fynelabs/fyneselfupdate v0.1.1 github.com/fynelabs/selfupdate v0.2.0 github.com/klauspost/compress v1.17.11 - github.com/rymdport/go-qrcode v1.1.0 github.com/rymdport/wormhole v0.1.1-0.20241116103349-4e36e05aff6c github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 9c961f4c..e585466d 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/rymdport/go-qrcode v1.1.0 h1:FBbvYA4pHGO+C22QmIx5wNVnlHQOGKOlXg9s6syskps= -github.com/rymdport/go-qrcode v1.1.0/go.mod h1:/DWqWDSfM/AcbuUr+/nNZev3SjO4bdZYqatpTpB9Js4= github.com/rymdport/portal v0.3.0 h1:QRHcwKwx3kY5JTQcsVhmhC3TGqGQb9LFghVNUy8AdB8= github.com/rymdport/portal v0.3.0/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= github.com/rymdport/wormhole v0.1.1-0.20241116103349-4e36e05aff6c h1:PEoCBrif+0g6CvQb9yXZefHqgJJn+W7gM8P3ksb5E9w= diff --git a/internal/ui/components/stack.go b/internal/ui/components/stack.go new file mode 100644 index 00000000..12daee53 --- /dev/null +++ b/internal/ui/components/stack.go @@ -0,0 +1,54 @@ +package components + +import ( + "fyne.io/fyne/v2" +) + +// StackNavigator represents a stack-based navigation manager +type StackNavigator struct { + stack []fyne.CanvasObject + current int + OnBack func() +} + +// NewNavigator creates a new Navigator instance. +func NewNavigator(initialPage fyne.CanvasObject) *StackNavigator { + return &StackNavigator{stack: []fyne.CanvasObject{initialPage}} +} + +// Next moves the view to the next view in the stack without adding contents. +// This allows viewes to move forwards through views without recreating each time. +func (n *StackNavigator) Next() { + if n.current == len(n.stack)-1 { + return + } + + n.current++ +} + +// Previous moves the view to the previous view in the stack without removing contents. +// This allows viewes to move backwards through views without recreating each time. +func (n *StackNavigator) Previous() { + if n.current == 0 || len(n.stack) < 1 { + + } + + n.current-- +} + +// Push adds a new page to the stack and displays it. +func (n *StackNavigator) Push(page fyne.CanvasObject) { + n.stack = append(n.stack, page) + n.Next() +} + +// Pop removes the current page and returns to the previous one. +func (n *StackNavigator) Pop() { + if len(n.stack) <= 1 { + return // Prevent popping the last page + } + + n.stack[len(n.stack)-1] = nil + n.stack = n.stack[:len(n.stack)-1] + n.Previous() +} From a87d786e41dab8362fffb8b0b29bb5e8ca2da7d5 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 16:47:52 +0100 Subject: [PATCH 08/16] Further project cleanup in preparation for rewrite --- go.mod | 2 +- internal/transport/receiver.go | 154 -------------------------------- internal/transport/sender.go | 112 ----------------------- internal/transport/transport.go | 31 ------- internal/util/util.go | 12 --- zip/zip.go | 135 ---------------------------- 6 files changed, 1 insertion(+), 445 deletions(-) delete mode 100644 internal/transport/receiver.go delete mode 100644 internal/transport/sender.go delete mode 100644 zip/zip.go diff --git a/go.mod b/go.mod index 0d55f88c..b2735a2c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763 github.com/fynelabs/fyneselfupdate v0.1.1 github.com/fynelabs/selfupdate v0.2.0 - github.com/klauspost/compress v1.17.11 github.com/rymdport/wormhole v0.1.1-0.20241116103349-4e36e05aff6c github.com/stretchr/testify v1.9.0 ) @@ -30,6 +29,7 @@ require ( github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rymdport/portal v0.3.0 // indirect diff --git a/internal/transport/receiver.go b/internal/transport/receiver.go deleted file mode 100644 index 7d8ded4e..00000000 --- a/internal/transport/receiver.go +++ /dev/null @@ -1,154 +0,0 @@ -package transport - -import ( - "context" - "errors" - "io" - "os" - "path/filepath" - "strconv" - - "fyne.io/fyne/v2" - "github.com/Jacalz/rymdport/v3/internal/util" - "github.com/Jacalz/rymdport/v3/zip" - "github.com/rymdport/wormhole/wormhole" -) - -var errorTooManyDuplicates = errors.New("too many duplicates found. Stopped trying to add new numbers to end") - -func bail(msg *wormhole.IncomingMessage, err error) error { - if msg == nil || msg.Type == wormhole.TransferText { // Rejecting text receives is not possible. - return err - } else if rerr := msg.Reject(); rerr != nil { - return rerr - } - - return err -} - -// NewReceive runs a receive using wormhole-william and handles types accordingly. -func (c *Client) NewReceive(code string) (*wormhole.IncomingMessage, error) { - msg, err := c.Receive(context.Background(), code) - if err != nil { - fyne.LogError("Error on receiving data", err) - return nil, bail(msg, err) - } - - return msg, nil -} - -// SaveToDisk saves the incomming file or directory transfer to the disk. -func (c *Client) SaveToDisk(msg *wormhole.IncomingMessage, targetPath string, progress func(int64, int64)) (err error) { - contents := util.NewProgressReader(msg, progress, msg.TransferBytes) - - if msg.Type == wormhole.TransferDirectory && c.NoExtractDirectory { - targetPath += ".zip" // We are saving the zip-file and not extracting. - } - - if !c.OverwriteExisting { - _, err := os.Stat(targetPath) - if err == nil || os.IsExist(err) { - targetPath, err = addFileIncrement(targetPath) - if err != nil { - fyne.LogError("Error on trying to create non-duplicate filename", err) - return bail(msg, err) - } - } - } - - if msg.Type == wormhole.TransferFile || c.NoExtractDirectory { - return writeToFile(targetPath, msg, contents) - } - - // We are reading the transferred bytes twice. First from msg to temp file and then from temp. - contents.Max *= 2 - - return writeToDirectory(targetPath, msg, contents, progress) -} - -func writeToDirectory(targetPath string, msg *wormhole.IncomingMessage, contents util.ProgressReader, progress func(int64, int64)) (err error) { - tmp, err := os.CreateTemp("", msg.Name+"-*.zip.tmp") - if err != nil { - fyne.LogError("Error on creating tempfile", err) - return bail(msg, err) - } - - defer func() { - if cerr := tmp.Close(); cerr != nil { - fyne.LogError("Error on closing file", err) - err = cerr - } - - if rerr := os.Remove(tmp.Name()); rerr != nil { - fyne.LogError("Error on removing temp file", err) - err = rerr - } - }() - - var n int64 - n, err = io.Copy(tmp, contents) - if err != nil { - fyne.LogError("Error on copying contents to file", err) - return - } - - err = zip.ExtractSafe(util.NewProgressReaderAt(tmp, progress, contents.Max), - n, targetPath, msg.UncompressedBytes, msg.FileCount) - if err != nil { - fyne.LogError("Error on unzipping contents", err) - return - } - - progress(0, 1) // Workaround for progress sometimes stopping at 99%. - - return -} - -func writeToFile(destination string, msg *wormhole.IncomingMessage, contents util.ProgressReader) (err error) { - file, err := os.Create(destination) // #nosec Path is cleaned by filepath.Join(). - if err != nil { - fyne.LogError("Error on creating file", err) - return bail(msg, err) - } - - defer func() { - if cerr := file.Close(); cerr != nil { - fyne.LogError("Error on closing file", err) - err = cerr - } - }() - - _, err = io.Copy(file, contents) - if err != nil { - fyne.LogError("Error on copying contents to file", err) - return - } - - return -} - -// addFileIncrement tries to add a number to the end of the filename if a duplicate exists. -// If it fails to do so after five tries, it till return the given path and an error. -func addFileIncrement(path string) (string, error) { - base := filepath.Dir(path) - ext := filepath.Ext(path) - name := filepath.Base(path) - name = name[:len(name)-len(ext)] - - // Add number at the end. Cap it to avoid doing checks forever. - for i := 1; i <= 5; i++ { - nr := strconv.Itoa(i) - incremented := filepath.Join(base, name+"("+nr+")"+ext) - - _, err := os.Stat(incremented) - if err != nil { - if os.IsNotExist(err) { - return incremented, nil - } - - return "", err - } - } - - return path, errorTooManyDuplicates -} diff --git a/internal/transport/sender.go b/internal/transport/sender.go deleted file mode 100644 index 55650391..00000000 --- a/internal/transport/sender.go +++ /dev/null @@ -1,112 +0,0 @@ -package transport - -import ( - "context" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - - "fyne.io/fyne/v2" - "github.com/Jacalz/rymdport/v3/zip" - "github.com/rymdport/wormhole/wormhole" -) - -// NewFileSend takes the chosen file and sends it using wormhole-william. -func (c *Client) NewFileSend(file fyne.URIReadCloser, progress wormhole.SendOption, code string) (string, chan wormhole.SendResult, error) { - return c.SendFile(context.Background(), file.URI().Name(), file.(io.ReadSeeker), progress, wormhole.WithCode(code)) -} - -// NewDirSend takes a listable URI and sends it using wormhole-william. -func (c *Client) NewDirSend(dir fyne.ListableURI, progress wormhole.SendOption, code string) (string, chan wormhole.SendResult, error) { - path := dir.Path() - prefixStr, _ := filepath.Split(path) - prefix := len(prefixStr) // Where the prefix ends. Doing it this way is faster and works when paths don't use same separator (\ or /). - - files, err := appendFilesFromPath([]wormhole.DirectoryEntry{}, path, prefix) - if err != nil { - return "", nil, err - } - - return c.SendDirectory(context.Background(), dir.Name(), files, progress, wormhole.WithCode(code)) -} - -// NewMultipleFileSend sends multiple files as a directory send using wormhole-william. -func (c *Client) NewMultipleFileSend(uris []fyne.URI, progress wormhole.SendOption, code string) (string, chan wormhole.SendResult, error) { - // We want to prefix all directory entires with the parent folder only. - baseDir := filepath.Dir(uris[0].Path()) - subDir, dirName := filepath.Split(baseDir) - prefix := len(subDir) - - files := make([]wormhole.DirectoryEntry, 0, len(uris)) - for _, uri := range uris { - absPath, err := filepath.Abs(filepath.Join(baseDir, uri.Name())) - if err != nil { - return "", nil, err - } - - if !strings.HasPrefix(absPath, baseDir) { - return "", nil, zip.ErrorDangerousFilename - } - - path := uri.Path() - info, err := os.Lstat(path) - if err != nil { - return "", nil, err - } - - if !info.IsDir() { - files = append(files, wormhole.DirectoryEntry{ - Path: path[prefix:], // Instead of strings.TrimPrefix. Paths don't need match separators (e.g. "C:/home/dir" == "C:\home\dir"). - Mode: info.Mode(), - Reader: func() (io.ReadCloser, error) { - return os.Open(filepath.Clean(path)) - }, - }) - - continue - } - - files, err = appendFilesFromPath(files, path, prefix) - if err != nil { - return "", nil, err - } - } - - return c.SendDirectory(context.Background(), dirName, files, progress, wormhole.WithCode(code)) -} - -// NewTextSend takes a text input and sends the text using wormhole-william. -func (c *Client) NewTextSend(text string, progress wormhole.SendOption, code string) (string, chan wormhole.SendResult, error) { - return c.SendText(context.Background(), text, progress, wormhole.WithCode(code)) -} - -func appendFilesFromPath(files []wormhole.DirectoryEntry, path string, prefixLength int) ([]wormhole.DirectoryEntry, error) { - err := filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } else if entry.IsDir() { - return nil - } - - info, err := entry.Info() - if err != nil { - return err - } else if !info.Mode().IsRegular() { - return nil - } - - files = append(files, wormhole.DirectoryEntry{ - Path: path[prefixLength:], // Instead of strings.TrimPrefix. Paths don't need match separators (e.g. "C:/home/dir" == "C:\home\dir"). - Mode: info.Mode(), - Reader: func() (io.ReadCloser, error) { - return os.Open(path) // #nosec - path is already cleaned by filepath.WalkDIr - }, - }) - - return nil - }) - - return files, err -} diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 850bcf30..4e4344a3 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -2,41 +2,10 @@ package transport import ( - "fyne.io/fyne/v2" "github.com/rymdport/wormhole/wormhole" ) // Client defines the client for handling sending and receiving using wormhole-william type Client struct { wormhole.Client - - // App is a reference to the currently running Fyne application. - App fyne.App - - // CustomCode defines if we should pass a custom code or let wormhole-william generate on for us. - CustomCode bool - - // DownloadPath holds the download path used for saving received files. - DownloadPath string - - // NoExtractDirectory specifies if we should extract directories or not. - NoExtractDirectory bool - - // Notification holds the settings value for if we have notifications enabled or not. - Notifications bool - - // OverwriteExisting holds the settings value for if we should overwrite already existing files. - OverwriteExisting bool -} - -// ShowNotification sends a notification if c.Notifications is true. -func (c *Client) ShowNotification(title, content string) { - if c.Notifications { - c.App.SendNotification(&fyne.Notification{Title: title, Content: content}) - } -} - -// NewClient returns a new client for sending and receiving using wormhole-william -func NewClient(app fyne.App) *Client { - return &Client{App: app} } diff --git a/internal/util/util.go b/internal/util/util.go index 8a6443b0..33510446 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -3,8 +3,6 @@ package util import ( "errors" - "os" - "path/filepath" "strings" "fyne.io/fyne/v2" @@ -61,16 +59,6 @@ func runeIsNotNumerical(r rune) bool { return r < '0' || r > '9' } -// UserDownloadsFolder returns the downloads folder corresponding to the current user. -func UserDownloadsFolder() string { - dir, err := os.UserHomeDir() - if err != nil { - fyne.LogError("Could not get home dir", err) - } - - return filepath.Join(dir, "Downloads") -} - // WindowSizeToDialog scales the window size to a suitable dialog size. func WindowSizeToDialog(s fyne.Size) fyne.Size { return fyne.NewSize(s.Width*0.8, s.Height*0.8) diff --git a/zip/zip.go b/zip/zip.go deleted file mode 100644 index ea8127d8..00000000 --- a/zip/zip.go +++ /dev/null @@ -1,135 +0,0 @@ -// Package zip contains an implementation of a zip extractor. -package zip - -import ( - "errors" - "io" - "os" - "path/filepath" - "strings" - - "fyne.io/fyne/v2" - "github.com/klauspost/compress/zip" -) - -var ( - // ErrorDangerousFilename indicates that a dangerous filename was found. - ErrorDangerousFilename = errors.New("dangerous filename detected") - - // ErrorSizeMismatch indicates that the uncompressed size was unexpected. - ErrorSizeMismatch = errors.New("mismatch between offered and actual size") - - // ErrorFileCountMismatch indicates that the file count was unexpected. - ErrorFileCountMismatch = errors.New("mismatch between offered and actual file count") -) - -// ExtractSafe works like Extract() but verifies that the uncompressed size and file count are as expected. -// This can only be used if you know the file count and uncompressed size before extracting. -func ExtractSafe(source io.ReaderAt, length int64, target string, uncompressedBytes int64, files int) error { - reader, err := zip.NewReader(source, length) - if err != nil { - fyne.LogError("Could not create zip reader", err) - return err - } - - // Check that the file count is as expected. - if files < len(reader.File) { - return ErrorFileCountMismatch - } - - // Check that the extracted size is as expected. - actualUncompressedSize := uint64(0) - for _, f := range reader.File { - actualUncompressedSize += f.FileHeader.UncompressedSize64 - } - if uncompressedBytes < 0 || actualUncompressedSize > uint64(uncompressedBytes) { - return ErrorSizeMismatch - } - - for _, file := range reader.File { - if err := extractFile(file, target); err != nil { - return err - } - } - - return nil -} - -// Extract takes a reader and the length and then extracts it to the target. -// The target should be the path to a folder where the extracted files can be placed. -func Extract(source io.ReaderAt, length int64, target string) error { - reader, err := zip.NewReader(source, length) - if err != nil { - fyne.LogError("Could not create zip reader", err) - return err - } - - for _, file := range reader.File { - if err := extractFile(file, target); err != nil { - return err - } - } - - return nil -} - -func extractFile(file *zip.File, target string) (err error) { - path, err := filepath.Abs(filepath.Join(target, file.Name)) - if err != nil { - fyne.LogError("Could not calculate the ABS path", err) - return err - } - - if !strings.HasPrefix(path, target) { - fyne.LogError("Dangerous filename detected", ErrorDangerousFilename) - return ErrorDangerousFilename - } - - fileReader, err := file.Open() - if err != nil { - fyne.LogError("Could not open the zip file", err) - return err - } - - defer func() { - if cerr := fileReader.Close(); cerr != nil { - fyne.LogError("Could not close the zip file reader", err) - err = cerr - } - }() - - if file.FileInfo().IsDir() { - if err := os.MkdirAll(path, 0o750); err != nil { - fyne.LogError("Could not create the directory", err) - return err - } - - return - } - - if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { - fyne.LogError("Could not create the directory", err) - return err - } - - targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) // #nosec - The path has already been cleaned by filepath.Abs() - if err != nil { - fyne.LogError("Could not create the target file", err) - return err - } - - defer func() { - if cerr := targetFile.Close(); cerr != nil { - fyne.LogError("Could not close the target file", err) - err = cerr - } - }() - - _, err = io.Copy(targetFile, fileReader) - if err != nil { - fyne.LogError("Could not copy the contents", err) - return err - } - - return -} From 2d5d86721e8d33805fefd20c0df17cc11e2a7f03 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 16:48:34 +0100 Subject: [PATCH 09/16] Run gofumpt on the code --- internal/ui/components/stack.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ui/components/stack.go b/internal/ui/components/stack.go index 12daee53..e2fdfce2 100644 --- a/internal/ui/components/stack.go +++ b/internal/ui/components/stack.go @@ -30,7 +30,6 @@ func (n *StackNavigator) Next() { // This allows viewes to move backwards through views without recreating each time. func (n *StackNavigator) Previous() { if n.current == 0 || len(n.stack) < 1 { - } n.current-- From f5289b330592d7f3414ba82116fad5fa94931e4c Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sat, 4 Jan 2025 17:49:34 +0100 Subject: [PATCH 10/16] Add settings to the dropdown menu --- internal/ui/setup.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/setup.go b/internal/ui/setup.go index e3149502..4a4757e0 100644 --- a/internal/ui/setup.go +++ b/internal/ui/setup.go @@ -15,7 +15,7 @@ func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { dropdown := &widget.Button{Icon: theme.MenuIcon(), Importance: widget.LowImportance} dropdown.OnTapped = func() { widget.ShowPopUpMenuAtRelativePosition( - &fyne.Menu{Items: []*fyne.MenuItem{{Label: "About", Icon: theme.InfoIcon(), Action: func() { + &fyne.Menu{Items: []*fyne.MenuItem{{Label: "Settings", Icon: theme.SettingsIcon()}, {Label: "About", Icon: theme.InfoIcon(), Action: func() { links := []*widget.Hyperlink{ {Text: "Repository", URL: util.URLToGitHubProject("")}, {Text: "Issue Tracker", URL: util.URLToGitHubProject("/issues")}, @@ -24,7 +24,7 @@ func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { abwin := dialog.NewAboutWindow("Easy encrypted file, folder, and text sharing between devices.", links, a) abwin.Resize(fyne.NewSize(500, 300)) abwin.Show() - }}}}, w.Canvas(), fyne.Position{Y: dropdown.Size().Height}, dropdown) + }}}}, w.Canvas(), fyne.Position{Y: dropdown.Size().Height + theme.Padding()}, dropdown) } tabs := &container.AppTabs{ From 2deb552850ccf9bcf2581a0a319d306f6a34ae2c Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 5 Jan 2025 11:45:12 +0100 Subject: [PATCH 11/16] Some minor cleanup of menu code --- internal/ui/menu.go | 35 +++++++++++++++++++++++++++++++++++ internal/ui/setup.go | 17 +++-------------- 2 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 internal/ui/menu.go diff --git a/internal/ui/menu.go b/internal/ui/menu.go new file mode 100644 index 00000000..23b2d09f --- /dev/null +++ b/internal/ui/menu.go @@ -0,0 +1,35 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "fyne.io/x/fyne/dialog" + "github.com/Jacalz/rymdport/v3/internal/util" +) + +func openSettingsMenu(a fyne.App) { + w := a.NewWindow("Settings") + w.SetContent(widget.NewLabel("Under contstruction...")) + w.Show() +} + +func openAboutMenu(a fyne.App) { + links := []*widget.Hyperlink{ + {Text: "Repository", URL: util.URLToGitHubProject("")}, + {Text: "Issue Tracker", URL: util.URLToGitHubProject("/issues")}, + {Text: "Wiki", URL: util.URLToGitHubProject("/wiki")}, + } + w := dialog.NewAboutWindow("Easy encrypted file, folder, and text sharing between devices.", links, a) + w.Resize(fyne.NewSize(500, 300)) + w.Show() +} + +func buildApplicationMenu(a fyne.App) *fyne.Menu { + menus := []*fyne.MenuItem{ + {Label: "Settings", Icon: theme.SettingsIcon(), Action: func() { openSettingsMenu(a) }}, + {Label: "About", Icon: theme.InfoIcon(), Action: func() { openAboutMenu(a) }}, + } + + return &fyne.Menu{Items: menus} +} diff --git a/internal/ui/setup.go b/internal/ui/setup.go index 4a4757e0..9943a10c 100644 --- a/internal/ui/setup.go +++ b/internal/ui/setup.go @@ -5,26 +5,15 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/util" - - "fyne.io/x/fyne/dialog" ) // Create sets up the user interface for the application. func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { + menu := buildApplicationMenu(a) dropdown := &widget.Button{Icon: theme.MenuIcon(), Importance: widget.LowImportance} dropdown.OnTapped = func() { - widget.ShowPopUpMenuAtRelativePosition( - &fyne.Menu{Items: []*fyne.MenuItem{{Label: "Settings", Icon: theme.SettingsIcon()}, {Label: "About", Icon: theme.InfoIcon(), Action: func() { - links := []*widget.Hyperlink{ - {Text: "Repository", URL: util.URLToGitHubProject("")}, - {Text: "Issue Tracker", URL: util.URLToGitHubProject("/issues")}, - {Text: "Wiki", URL: util.URLToGitHubProject("/wiki")}, - } - abwin := dialog.NewAboutWindow("Easy encrypted file, folder, and text sharing between devices.", links, a) - abwin.Resize(fyne.NewSize(500, 300)) - abwin.Show() - }}}}, w.Canvas(), fyne.Position{Y: dropdown.Size().Height + theme.Padding()}, dropdown) + offset := fyne.Position{Y: dropdown.Size().Height + theme.Padding()} + widget.ShowPopUpMenuAtRelativePosition(menu, w.Canvas(), offset, dropdown) } tabs := &container.AppTabs{ From 82f06cb6668032d0be99f22d01ba6a77fee62e58 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 5 Jan 2025 12:58:46 +0100 Subject: [PATCH 12/16] Implement a stack navigator --- internal/ui/components/stack.go | 145 ++++++++++++++++++++++++++------ internal/ui/recv.go | 18 +++- internal/ui/send.go | 30 ++++++- internal/ui/setup.go | 11 ++- 4 files changed, 167 insertions(+), 37 deletions(-) diff --git a/internal/ui/components/stack.go b/internal/ui/components/stack.go index e2fdfce2..d64c1de1 100644 --- a/internal/ui/components/stack.go +++ b/internal/ui/components/stack.go @@ -2,52 +2,143 @@ package components import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" ) +var _ fyne.Widget = (*StackNavigator)(nil) + // StackNavigator represents a stack-based navigation manager type StackNavigator struct { - stack []fyne.CanvasObject - current int - OnBack func() + widget.BaseWidget + stack []fyne.CanvasObject + titles []string + OnBack func() } // NewNavigator creates a new Navigator instance. -func NewNavigator(initialPage fyne.CanvasObject) *StackNavigator { - return &StackNavigator{stack: []fyne.CanvasObject{initialPage}} +func NewNavigator(initial fyne.CanvasObject) *StackNavigator { + return &StackNavigator{stack: []fyne.CanvasObject{initial}, titles: []string{""}} +} + +// Push adds a new page to the stack and displays it. +func (n *StackNavigator) Push(page fyne.CanvasObject, title string) { + n.stack = append(n.stack, page) + n.titles = append(n.titles, title) + n.Refresh() } -// Next moves the view to the next view in the stack without adding contents. -// This allows viewes to move forwards through views without recreating each time. -func (n *StackNavigator) Next() { - if n.current == len(n.stack)-1 { - return +// Pop removes the current page and returns to the previous one. +func (n *StackNavigator) Pop() { + if len(n.stack) <= 1 { + return // Prevent popping the last page. } - n.current++ + n.stack[len(n.stack)-1] = nil + n.stack = n.stack[:len(n.stack)-1] + n.titles = n.titles[:len(n.titles)-1] + + n.Refresh() } -// Previous moves the view to the previous view in the stack without removing contents. -// This allows viewes to move backwards through views without recreating each time. -func (n *StackNavigator) Previous() { - if n.current == 0 || len(n.stack) < 1 { +func (n *StackNavigator) MinSize() fyne.Size { + n.ExtendBaseWidget(n) + return n.BaseWidget.MinSize() +} + +// CreateRenderer creats the stackNavigatorRenderer. +func (n *StackNavigator) CreateRenderer() fyne.WidgetRenderer { + renderer := &stackNavigatorRenderer{ + parent: n, + backButton: widget.Button{ + Icon: theme.NavigateBackIcon(), + Text: "Go back", + Importance: widget.LowImportance, + OnTapped: n.OnBack, + }, + titleLabel: widget.Label{ + Text: n.titles[len(n.titles)-1], + TextStyle: fyne.TextStyle{Bold: true}, + Alignment: fyne.TextAlignCenter, + }, } - n.current-- + renderer.backButton.Hidden = len(n.stack) == 1 + renderer.titleLabel.Hidden = renderer.backButton.Hidden + renderer.separator.Hidden = renderer.backButton.Hidden + + renderer.objects = []fyne.CanvasObject{&renderer.backButton, &renderer.titleLabel, &renderer.separator, n.stack[len(n.stack)-1]} + return renderer } -// Push adds a new page to the stack and displays it. -func (n *StackNavigator) Push(page fyne.CanvasObject) { - n.stack = append(n.stack, page) - n.Next() +var _ fyne.WidgetRenderer = (*stackNavigatorRenderer)(nil) + +type stackNavigatorRenderer struct { + parent *StackNavigator + objects []fyne.CanvasObject + + backButton widget.Button + titleLabel widget.Label + separator widget.Separator } -// Pop removes the current page and returns to the previous one. -func (n *StackNavigator) Pop() { - if len(n.stack) <= 1 { - return // Prevent popping the last page +func (r *stackNavigatorRenderer) Destroy() { +} + +// Layout is a hook that is called if the widget needs to be laid out. +// This should never call [Refresh]. +func (r *stackNavigatorRenderer) Layout(size fyne.Size) { + contentStartsAt := float32(0) + if len(r.parent.stack) > 1 { + r.backButton.Move(fyne.Position{}) + buttonSize := r.backButton.MinSize() + r.backButton.Resize(buttonSize) + + labelSize := r.titleLabel.MinSize() + r.titleLabel.Move(fyne.NewPos((size.Width-labelSize.Width)/2, 0)) + r.titleLabel.Resize(labelSize) + + contentStartsAt = buttonSize.Height + theme.Padding() + + r.separator.Move(fyne.Position{Y: contentStartsAt}) + r.separator.Resize(fyne.NewSize(size.Width, theme.SeparatorThicknessSize())) + } - n.stack[len(n.stack)-1] = nil - n.stack = n.stack[:len(n.stack)-1] - n.Previous() + r.objects[3].Move(fyne.NewPos(0, contentStartsAt)) + r.objects[3].Resize(size.SubtractWidthHeight(0, contentStartsAt)) +} + +// MinSize returns the minimum size of the widget that is rendered by this renderer. +func (r *stackNavigatorRenderer) MinSize() fyne.Size { + minSize := r.objects[3].MinSize() + if len(r.parent.stack) > 1 { + return minSize.AddWidthHeight(0, r.backButton.MinSize().Height+theme.Padding()) + } + + return minSize +} + +// Objects returns all objects that should be drawn. +func (r *stackNavigatorRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// Refresh is a hook that is called if the widget has updated and needs to be redrawn. +// This might trigger a [Layout]. +func (r *stackNavigatorRenderer) Refresh() { + r.titleLabel.Text = r.parent.titles[len(r.parent.titles)-1] + r.titleLabel.Hidden = len(r.parent.stack) == 1 + r.titleLabel.Refresh() + + r.backButton.Hidden = r.titleLabel.Hidden + r.backButton.Refresh() + + r.separator.Hidden = r.titleLabel.Hidden + r.separator.Refresh() + + r.objects[3] = r.parent.stack[len(r.parent.stack)-1] + + canvas.Refresh(r.parent) } diff --git a/internal/ui/recv.go b/internal/ui/recv.go index c6b21940..56c2c07a 100644 --- a/internal/ui/recv.go +++ b/internal/ui/recv.go @@ -1,23 +1,35 @@ package ui import ( + "github.com/Jacalz/rymdport/v3/internal/ui/components" + "github.com/Jacalz/rymdport/v3/internal/util" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/Jacalz/rymdport/v3/internal/util" ) -func createRecvPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { +func buildRecvView() fyne.CanvasObject { + return widget.NewLabel("Receiving will be implemented soon...") +} + +func createRecvPage(navigator *components.StackNavigator) fyne.CanvasObject { icon := canvas.NewImageFromResource(theme.DownloadIcon()) icon.FillMode = canvas.ImageFillContain icon.SetMinSize(fyne.NewSquareSize(200)) description := &widget.Label{Text: "Enter a code below to start receiving data.", Alignment: fyne.TextAlignCenter} + recvView := buildRecvView() code := &widget.Entry{PlaceHolder: "Code from sender", Validator: util.CodeValidator} - start := &widget.Button{Text: "Start Receive", Icon: theme.DownloadIcon(), Importance: widget.HighImportance} + start := &widget.Button{ + Text: "Start Receive", + Icon: theme.DownloadIcon(), + Importance: widget.HighImportance, + OnTapped: func() { navigator.Push(recvView, "Receiving Data") }, + } content := container.NewVBox(icon, description, &widget.Separator{}, code, &widget.Separator{}, container.NewCenter(start)) diff --git a/internal/ui/send.go b/internal/ui/send.go index b2795b8e..26bceb0c 100644 --- a/internal/ui/send.go +++ b/internal/ui/send.go @@ -1,6 +1,8 @@ package ui import ( + "github.com/Jacalz/rymdport/v3/internal/ui/components" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" @@ -8,16 +10,36 @@ import ( "fyne.io/fyne/v2/widget" ) -func createSendPage(_ fyne.App, _ fyne.Window) fyne.CanvasObject { +func buildSendView() fyne.CanvasObject { + return widget.NewLabel("Sending will be implemented soon...") +} + +func createSendPage(navigator *components.StackNavigator) fyne.CanvasObject { icon := canvas.NewImageFromResource(theme.UploadIcon()) icon.FillMode = canvas.ImageFillContain icon.SetMinSize(fyne.NewSquareSize(200)) description := &widget.Label{Text: "Select data type below or drop files here.", Alignment: fyne.TextAlignCenter} - file := &widget.Button{Icon: theme.FileTextIcon(), Text: "Send File", Importance: widget.HighImportance} - folder := &widget.Button{Icon: theme.FolderIcon(), Text: "Send Folder", Importance: widget.HighImportance} - text := &widget.Button{Icon: theme.DocumentIcon(), Text: "Send Text", Importance: widget.HighImportance} + sendView := buildSendView() + file := &widget.Button{ + Icon: theme.FileTextIcon(), + Text: "Send File", + Importance: widget.HighImportance, + OnTapped: func() { navigator.Push(sendView, "Sending File") }, + } + folder := &widget.Button{ + Icon: theme.FolderIcon(), + Text: "Send Folder", + Importance: widget.HighImportance, + OnTapped: func() { navigator.Push(sendView, "Sending Folder") }, + } + text := &widget.Button{ + Icon: theme.DocumentIcon(), + Text: "Send Text", + Importance: widget.HighImportance, + OnTapped: func() { navigator.Push(sendView, "Sending Text") }, + } buttons := container.NewCenter(container.NewHBox(file, &widget.Separator{}, folder, &widget.Separator{}, text)) content := container.NewVBox(icon, description, &widget.Separator{}, buttons) diff --git a/internal/ui/setup.go b/internal/ui/setup.go index 9943a10c..65c64374 100644 --- a/internal/ui/setup.go +++ b/internal/ui/setup.go @@ -1,6 +1,8 @@ package ui import ( + "github.com/Jacalz/rymdport/v3/internal/ui/components" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" @@ -16,13 +18,16 @@ func Create(a fyne.App, w fyne.Window) fyne.CanvasObject { widget.ShowPopUpMenuAtRelativePosition(menu, w.Canvas(), offset, dropdown) } + navigator := &components.StackNavigator{} + navigator.OnBack = navigator.Pop tabs := &container.AppTabs{ Items: []*container.TabItem{ - {Text: "Send", Icon: theme.UploadIcon(), Content: createSendPage(a, w)}, - {Text: "Receive", Icon: theme.DownloadIcon(), Content: createRecvPage(a, w)}, + {Text: "Send", Icon: theme.UploadIcon(), Content: createSendPage(navigator)}, + {Text: "Receive", Icon: theme.DownloadIcon(), Content: createRecvPage(navigator)}, }, } upperRightCorner := container.NewBorder(container.NewBorder(nil, nil, nil, dropdown), nil, nil, nil) - return container.NewStack(tabs, upperRightCorner) + navigator.Push(container.NewStack(tabs, upperRightCorner), "") + return navigator } From de44bfa37c43cb31569124e9d88ff0c9903d8b65 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 5 Jan 2025 13:01:35 +0100 Subject: [PATCH 13/16] Clean up navigator code a bit --- internal/ui/components/stack.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/internal/ui/components/stack.go b/internal/ui/components/stack.go index d64c1de1..2869d457 100644 --- a/internal/ui/components/stack.go +++ b/internal/ui/components/stack.go @@ -2,7 +2,6 @@ package components import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -64,9 +63,10 @@ func (n *StackNavigator) CreateRenderer() fyne.WidgetRenderer { }, } - renderer.backButton.Hidden = len(n.stack) == 1 - renderer.titleLabel.Hidden = renderer.backButton.Hidden - renderer.separator.Hidden = renderer.backButton.Hidden + hideNavbar := len(n.stack) == 1 + renderer.backButton.Hidden = hideNavbar + renderer.titleLabel.Hidden = hideNavbar + renderer.separator.Hidden = hideNavbar renderer.objects = []fyne.CanvasObject{&renderer.backButton, &renderer.titleLabel, &renderer.separator, n.stack[len(n.stack)-1]} return renderer @@ -83,8 +83,7 @@ type stackNavigatorRenderer struct { separator widget.Separator } -func (r *stackNavigatorRenderer) Destroy() { -} +func (r *stackNavigatorRenderer) Destroy() {} // Layout is a hook that is called if the widget needs to be laid out. // This should never call [Refresh]. @@ -128,17 +127,17 @@ func (r *stackNavigatorRenderer) Objects() []fyne.CanvasObject { // Refresh is a hook that is called if the widget has updated and needs to be redrawn. // This might trigger a [Layout]. func (r *stackNavigatorRenderer) Refresh() { + hideNavbar := len(r.parent.stack) == 1 + r.titleLabel.Text = r.parent.titles[len(r.parent.titles)-1] - r.titleLabel.Hidden = len(r.parent.stack) == 1 + r.titleLabel.Hidden = hideNavbar r.titleLabel.Refresh() - r.backButton.Hidden = r.titleLabel.Hidden + r.backButton.Hidden = hideNavbar r.backButton.Refresh() - r.separator.Hidden = r.titleLabel.Hidden + r.separator.Hidden = hideNavbar r.separator.Refresh() r.objects[3] = r.parent.stack[len(r.parent.stack)-1] - - canvas.Refresh(r.parent) } From fd14a2ce2fbbbd4fdef6bfeb29fae2e9c494fab4 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 5 Jan 2025 13:07:10 +0100 Subject: [PATCH 14/16] Bump minimum Go version to 1.22 --- SECURITY.md | 4 ++-- go.mod | 2 +- internal/transport/completion_test.go | 2 +- internal/updater/automatic.go | 1 - internal/util/reader_test.go | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 502ff579..fc5a08c8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,8 +9,8 @@ If a new Go compiler version is released with security fixes, we will issue reco | Version | Supported | | ------- | ------------------ | -| 3.7.x | :white_check_mark: | -| < 3.7.0 | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0.0 | :x: | ## Reporting a Vulnerability diff --git a/go.mod b/go.mod index b2735a2c..337482f9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Jacalz/rymdport/v3 -go 1.21 +go 1.22 require ( fyne.io/fyne/v2 v2.5.3 diff --git a/internal/transport/completion_test.go b/internal/transport/completion_test.go index 80d20349..e86900c2 100644 --- a/internal/transport/completion_test.go +++ b/internal/transport/completion_test.go @@ -13,7 +13,7 @@ func BenchmarkNameplateCompletion(b *testing.B) { local := []string{} - for i := 0; i < b.N; i++ { + for range b.N { local = c.GenerateCodeCompletion("1-letterhead-be") } diff --git a/internal/updater/automatic.go b/internal/updater/automatic.go index ba2129c7..43266bf8 100644 --- a/internal/updater/automatic.go +++ b/internal/updater/automatic.go @@ -26,6 +26,5 @@ func Enable(a fyne.App, w fyne.Window) { _, err := selfupdate.Manage(config) if err != nil { fyne.LogError("Error while setting up update manager: ", err) - return } } diff --git a/internal/util/reader_test.go b/internal/util/reader_test.go index 09665fd8..971df8e2 100644 --- a/internal/util/reader_test.go +++ b/internal/util/reader_test.go @@ -18,7 +18,7 @@ func TestProgressReader(t *testing.T) { }, size) temp := [1]byte{} - for i := int64(0); i < size; i++ { + for i := range size { _, err := teeReader.Read(temp[:]) if err != nil { t.Fatal(err) From 61aac5d4d00300476e0058d59704418269646bba Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 5 Jan 2025 13:07:53 +0100 Subject: [PATCH 15/16] Use 1.22 in workflow as well --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d15fe651..38aba722 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.21.x', 'stable'] + go-version: ['1.22.x', 'stable'] steps: - uses: actions/checkout@v4 From f98e09dfd82ab270956c7c4db19808434e429060 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Mon, 6 Jan 2025 14:27:01 +0100 Subject: [PATCH 16/16] Update to latest fyne.io/x/fyne package --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 337482f9..d013994f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( fyne.io/fyne/v2 v2.5.3 - fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763 + fyne.io/x/fyne v0.0.0-20250106132206-3228f6c50107 github.com/fynelabs/fyneselfupdate v0.1.1 github.com/fynelabs/selfupdate v0.2.0 github.com/rymdport/wormhole v0.1.1-0.20241116103349-4e36e05aff6c @@ -37,7 +37,7 @@ require ( github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/yuin/goldmark v1.7.1 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/image v0.18.0 // indirect + golang.org/x/image v0.23.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/go.sum b/go.sum index e585466d..e53e9fa7 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ fyne.io/fyne/v2 v2.5.3 h1:k6LjZx6EzRZhClsuzy6vucLZBstdH2USDGHSGWq8ly8= fyne.io/fyne/v2 v2.5.3/go.mod h1:0GOXKqyvNwk3DLmsFu9v0oYM0ZcD1ysGnlHCerKoAmo= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= -fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763 h1:eKeSmgu0Ege3moDYC7ZqYB7ypOgiUzV/cyt1GAAJ/A8= -fyne.io/x/fyne v0.0.0-20241224222935-5be10b3dd763/go.mod h1:pWJAjc53T8QL7CBpByv7zFUI4Au5Pbh6JC30eViEHVU= +fyne.io/x/fyne v0.0.0-20250106132206-3228f6c50107 h1:hSbnee3f6u0PRh8QnR8q4lrN6nW4WSiSHmlGIV0ola8= +fyne.io/x/fyne v0.0.0-20250106132206-3228f6c50107/go.mod h1:wABgsyWBQOG0Pg5gCki7FmJhmhc5VZ4lfzel4gNHjFM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -335,8 +335,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=