Rewrite itgui and add new screenshots
| @@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) { | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		// Create new label containing error text | ||||
| 		errLbl := widget.NewLabel(err.Error()) | ||||
| 		errEntry := widget.NewEntry() | ||||
| 		errEntry.SetText(err.Error()) | ||||
| 		// If text changed, change it back | ||||
| 		errEntry.OnChanged = func(string) { | ||||
| 			errEntry.SetText(err.Error()) | ||||
| 		} | ||||
| 		// Create new dropdown containing error label | ||||
| 		content.Add(widget.NewAccordion( | ||||
| 			widget.NewAccordionItem("More Details", errLbl), | ||||
| 			widget.NewAccordionItem("More Details", errEntry), | ||||
| 		)) | ||||
| 	} | ||||
| 	if fatal { | ||||
| @@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) { | ||||
| 		// Show error dialog | ||||
| 		dialog.NewCustom("Error", "Ok", content, parent).Show() | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										163
									
								
								cmd/itgui/firmware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,163 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/dialog" | ||||
| 	"fyne.io/fyne/v2/layout" | ||||
| 	"fyne.io/fyne/v2/storage" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject { | ||||
| 	// Create select to chose between archive and files upgrade | ||||
| 	typeSelect := widget.NewSelect([]string{"Archive", "Files"}, nil) | ||||
| 	typeSelect.PlaceHolder = "Upgrade Type" | ||||
|  | ||||
| 	// Create map to store files | ||||
| 	files := map[string]string{} | ||||
|  | ||||
| 	// Create and disable start button | ||||
| 	startBtn := widget.NewButton("Start", nil) | ||||
| 	startBtn.Disable() | ||||
|  | ||||
| 	// Create new file open dialog for archive | ||||
| 	archiveDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) { | ||||
| 		if err != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		defer uc.Close() | ||||
| 		// Set archive path in map | ||||
| 		files[".zip"] = uc.URI().Path() | ||||
| 		// Enable start button | ||||
| 		startBtn.Enable() | ||||
| 	}, w) | ||||
| 	// Only allow .zip files | ||||
| 	archiveDlg.SetFilter(storage.NewExtensionFileFilter([]string{".zip"})) | ||||
| 	// Create button to show dialog | ||||
| 	archiveBtn := widget.NewButton("Select Archive (.zip)", archiveDlg.Show) | ||||
|  | ||||
| 	// Create new file open dialog for firmware image | ||||
| 	imageDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) { | ||||
| 		if err != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		defer uc.Close() | ||||
|  | ||||
| 		// Set firmware image path in map | ||||
| 		files[".bin"] = uc.URI().Path() | ||||
|  | ||||
| 		// If the init packet was already selected | ||||
| 		_, datOk := files[".dat"] | ||||
| 		if datOk { | ||||
| 			// Enable start button | ||||
| 			startBtn.Enable() | ||||
| 		} | ||||
| 	}, w) | ||||
| 	// Only allow .bin files | ||||
| 	imageDlg.SetFilter(storage.NewExtensionFileFilter([]string{".bin"})) | ||||
| 	// Create button to show dialog | ||||
| 	imageBtn := widget.NewButton("Select Firmware (.bin)", imageDlg.Show) | ||||
|  | ||||
| 	// Create new file open dialog for init packet | ||||
| 	initDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) { | ||||
| 		if err != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		defer uc.Close() | ||||
|  | ||||
| 		// Set init packet path in map | ||||
| 		files[".dat"] = uc.URI().Path() | ||||
|  | ||||
| 		// If the firmware image was already selected | ||||
| 		_, binOk := files[".bin"] | ||||
| 		if binOk { | ||||
| 			// Enable start button | ||||
| 			startBtn.Enable() | ||||
| 		} | ||||
| 	}, w) | ||||
| 	// Only allow .dat files | ||||
| 	initDlg.SetFilter(storage.NewExtensionFileFilter([]string{".dat"})) | ||||
| 	// Create button to show dialog | ||||
| 	initBtn := widget.NewButton("Select Init Packet (.dat)", initDlg.Show) | ||||
|  | ||||
| 	var upgType api.UpgradeType = 255 | ||||
| 	// When upgrade type changes | ||||
| 	typeSelect.OnChanged = func(s string) { | ||||
| 		// Delete all files from map | ||||
| 		delete(files, ".bin") | ||||
| 		delete(files, ".dat") | ||||
| 		delete(files, ".zip") | ||||
| 		// Hide all dialog buttons | ||||
| 		imageBtn.Hide() | ||||
| 		initBtn.Hide() | ||||
| 		archiveBtn.Hide() | ||||
| 		// Disable start button | ||||
| 		startBtn.Disable() | ||||
|  | ||||
| 		switch s { | ||||
| 		case "Files": | ||||
| 			// Set file upgrade type | ||||
| 			upgType = api.UpgradeTypeFiles | ||||
| 			// Show firmware image and init packet buttons | ||||
| 			imageBtn.Show() | ||||
| 			initBtn.Show() | ||||
| 		case "Archive": | ||||
| 			// Set archive upgrade type | ||||
| 			upgType = api.UpgradeTypeArchive | ||||
| 			// Show archive button | ||||
| 			archiveBtn.Show() | ||||
| 		} | ||||
| 	} | ||||
| 	// Select archive by default | ||||
| 	typeSelect.SetSelectedIndex(0) | ||||
|  | ||||
| 	// When start button pressed | ||||
| 	startBtn.OnTapped = func() { | ||||
| 		var args []string | ||||
| 		// Append the appropriate files for upgrade type | ||||
| 		switch upgType { | ||||
| 		case api.UpgradeTypeArchive: | ||||
| 			args = append(args, files[".zip"]) | ||||
| 		case api.UpgradeTypeFiles: | ||||
| 			args = append(args, files[".dat"], files[".bin"]) | ||||
| 		} | ||||
|  | ||||
| 		// If args are nil (invalid upgrade type) | ||||
| 		if args == nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Create new progress dialog | ||||
| 		progress := newProgress(w) | ||||
| 		// Start firmware upgrade | ||||
| 		progressCh, err := client.FirmwareUpgrade(ctx, upgType, args...) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error performing firmware upgrade", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 		// Show progress dialog | ||||
| 		progress.Show() | ||||
| 		// For every progress event | ||||
| 		for progressEvt := range progressCh { | ||||
| 			// Set progress bar values | ||||
| 			progress.SetTotal(float64(progressEvt.Total)) | ||||
| 			progress.SetValue(float64(progressEvt.Sent)) | ||||
| 		} | ||||
| 		// Hide progress dialog | ||||
| 		progress.Hide() | ||||
| 	} | ||||
|  | ||||
| 	return container.NewVBox( | ||||
| 		layout.NewSpacer(), | ||||
| 		typeSelect, | ||||
| 		archiveBtn, | ||||
| 		imageBtn, | ||||
| 		initBtn, | ||||
| 		startBtn, | ||||
| 		layout.NewSpacer(), | ||||
| 	) | ||||
| } | ||||
							
								
								
									
										360
									
								
								cmd/itgui/fs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,360 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/data/binding" | ||||
| 	"fyne.io/fyne/v2/dialog" | ||||
| 	"fyne.io/fyne/v2/theme" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject { | ||||
| 	c := container.NewVBox() | ||||
|  | ||||
| 	// Create new binding to store current directory | ||||
| 	cwdData := binding.NewString() | ||||
| 	cwdData.Set("/") | ||||
|  | ||||
| 	// Create new list binding to store fs listing entries | ||||
| 	lsData := binding.NewUntypedList() | ||||
|  | ||||
| 	// This goroutine waits until the fs tab is opened to | ||||
| 	// request the listing from the watch | ||||
| 	go func() { | ||||
| 		// Wait for opened signal | ||||
| 		<-opened | ||||
|  | ||||
| 		// Show loading pop up | ||||
| 		loading := newLoadingPopUp(w) | ||||
| 		loading.Show() | ||||
|  | ||||
| 		// Read root directory | ||||
| 		ls, err := client.ReadDir(ctx, "/") | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error reading directory", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 		// Set ls binding | ||||
| 		lsData.Set(lsToAny(ls)) | ||||
|  | ||||
| 		// Hide loading pop up | ||||
| 		loading.Hide() | ||||
| 	}() | ||||
|  | ||||
| 	toolbar := widget.NewToolbar( | ||||
| 		widget.NewToolbarAction( | ||||
| 			theme.ViewRefreshIcon(), | ||||
| 			func() { | ||||
| 				refresh(ctx, cwdData, lsData, client, w, c) | ||||
| 			}, | ||||
| 		), | ||||
| 		widget.NewToolbarAction( | ||||
| 			theme.UploadIcon(), | ||||
| 			func() { | ||||
| 				// Create open dialog for file that will be uploaded | ||||
| 				dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) { | ||||
| 					if err != nil || uc == nil { | ||||
| 						return | ||||
| 					} | ||||
| 					// Get filepath and close | ||||
| 					localPath := uc.URI().Path() | ||||
| 					uc.Close() | ||||
|  | ||||
| 					// Create new entry to store filepath | ||||
| 					filenameEntry := widget.NewEntry() | ||||
| 					// Set entry text to the file name of the selected file | ||||
| 					filenameEntry.SetText(filepath.Base(localPath)) | ||||
| 					// Create new dialog asking for the filename of the file to be stored on the watch | ||||
| 					uploadDlg := dialog.NewForm("Upload", "Upload", "Cancel", []*widget.FormItem{ | ||||
| 						widget.NewFormItem("Filename", filenameEntry), | ||||
| 					}, func(ok bool) { | ||||
| 						if !ok { | ||||
| 							return | ||||
| 						} | ||||
|  | ||||
| 						// Get current directory | ||||
| 						cwd, _ := cwdData.Get() | ||||
| 						// Get remote path by joining current directory with filename | ||||
| 						remotePath := filepath.Join(cwd, filenameEntry.Text) | ||||
|  | ||||
| 						// Create new progress dialog | ||||
| 						progressDlg := newProgress(w) | ||||
| 						progressDlg.Show() | ||||
|  | ||||
| 						// Upload file | ||||
| 						progressCh, err := client.Upload(ctx, remotePath, localPath) | ||||
| 						if err != nil { | ||||
| 							guiErr(err, "Error uploading file", false, w) | ||||
| 							return | ||||
| 						} | ||||
|  | ||||
| 						for progressEvt := range progressCh { | ||||
| 							progressDlg.SetTotal(float64(progressEvt.Total)) | ||||
| 							progressDlg.SetValue(float64(progressEvt.Sent)) | ||||
| 							if progressEvt.Sent == progressEvt.Total { | ||||
| 								break | ||||
| 							} | ||||
| 						} | ||||
|  | ||||
| 						// Close progress dialog | ||||
| 						progressDlg.Hide() | ||||
|  | ||||
| 						// Add file to listing (avoids full refresh) | ||||
| 						lsData.Append(api.FileInfo{ | ||||
| 							IsDir: false, | ||||
| 							Name:  filepath.Base(remotePath), | ||||
| 						}) | ||||
| 					}, w) | ||||
| 					uploadDlg.Show() | ||||
| 				}, w) | ||||
| 				dlg.Show() | ||||
|  | ||||
| 			}, | ||||
| 		), | ||||
| 		widget.NewToolbarAction( | ||||
| 			theme.FolderNewIcon(), | ||||
| 			func() { | ||||
| 				// Create new entry for filename | ||||
| 				filenameEntry := widget.NewEntry() | ||||
| 				// Create new dialog to ask for the filename | ||||
| 				mkdirDialog := dialog.NewForm("Make Directory", "Create", "Cancel", []*widget.FormItem{ | ||||
| 					widget.NewFormItem("Filename", filenameEntry), | ||||
| 				}, func(ok bool) { | ||||
| 					if !ok { | ||||
| 						return | ||||
| 					} | ||||
|  | ||||
| 					// Get current directory | ||||
| 					cwd, _ := cwdData.Get() | ||||
| 					// Get remote path by joining current directory and filename | ||||
| 					remotePath := filepath.Join(cwd, filenameEntry.Text) | ||||
|  | ||||
| 					// Make directory | ||||
| 					err := client.Mkdir(ctx, remotePath) | ||||
| 					if err != nil { | ||||
| 						guiErr(err, "Error creating directory", false, w) | ||||
| 						return | ||||
| 					} | ||||
|  | ||||
| 					// Add directory to listing (avoids full refresh) | ||||
| 					lsData.Append(api.FileInfo{ | ||||
| 						IsDir: true, | ||||
| 						Name:  filepath.Base(remotePath), | ||||
| 					}) | ||||
| 				}, w) | ||||
| 				mkdirDialog.Show() | ||||
| 			}, | ||||
| 		), | ||||
| 	) | ||||
|  | ||||
| 	// Add listener to listing data to create the new items on the GUI | ||||
| 	// whenever the listing changes | ||||
| 	lsData.AddListener(binding.NewDataListener(func() { | ||||
| 		c.Objects = makeItems(ctx, client, lsData, cwdData, w, c) | ||||
| 		c.Refresh() | ||||
| 	})) | ||||
|  | ||||
| 	return container.NewBorder( | ||||
| 		nil, | ||||
| 		toolbar, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		container.NewVScroll(c), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| // makeItems creates GUI objects from listing data | ||||
| func makeItems( | ||||
| 	ctx context.Context, | ||||
| 	client *api.Client, | ||||
| 	lsData binding.UntypedList, | ||||
| 	cwdData binding.String, | ||||
| 	w fyne.Window, | ||||
| 	c *fyne.Container, | ||||
| ) []fyne.CanvasObject { | ||||
| 	// Get listing data | ||||
| 	ls, _ := lsData.Get() | ||||
|  | ||||
| 	// Create output slice with dame length as listing | ||||
| 	out := make([]fyne.CanvasObject, len(ls)) | ||||
| 	for index, val := range ls { | ||||
| 		// Assert value as file info | ||||
| 		item := val.(api.FileInfo) | ||||
|  | ||||
| 		var icon fyne.Resource | ||||
| 		// Decide which icon to use | ||||
| 		if item.IsDir { | ||||
| 			if item.Name == ".." { | ||||
| 				icon = theme.NavigateBackIcon() | ||||
| 			} else { | ||||
| 				icon = theme.FolderIcon() | ||||
| 			} | ||||
| 		} else { | ||||
| 			icon = theme.FileIcon() | ||||
| 		} | ||||
|  | ||||
| 		// Create new button with the decided icon and the item name | ||||
| 		btn := widget.NewButtonWithIcon(item.Name, icon, nil) | ||||
| 		// Align left | ||||
| 		btn.Alignment = widget.ButtonAlignLeading | ||||
| 		// Decide which callback function to use | ||||
| 		if item.IsDir { | ||||
| 			btn.OnTapped = func() { | ||||
| 				// Get current directory | ||||
| 				cwd, _ := cwdData.Get() | ||||
| 				// Join current directory with item name | ||||
| 				cwd = filepath.Join(cwd, item.Name) | ||||
| 				// Set new current directory | ||||
| 				cwdData.Set(cwd) | ||||
| 				// Refresh GUI to display new directory | ||||
| 				refresh(ctx, cwdData, lsData, client, w, c) | ||||
| 			} | ||||
| 		} else { | ||||
| 			btn.OnTapped = func() { | ||||
| 				// Get current directory | ||||
| 				cwd, _ := cwdData.Get() | ||||
| 				// Join current directory with item name | ||||
| 				remotePath := filepath.Join(cwd, item.Name) | ||||
| 				// Create new save dialog | ||||
| 				dlg := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) { | ||||
| 					if err != nil || uc == nil { | ||||
| 						return | ||||
| 					} | ||||
| 					// Get path of selected file | ||||
| 					localPath := uc.URI().Path() | ||||
| 					// Close WriteCloser (it's not needed) | ||||
| 					uc.Close() | ||||
|  | ||||
| 					// Create new progress dialog | ||||
| 					progressDlg := newProgress(w) | ||||
| 					progressDlg.Show() | ||||
|  | ||||
| 					// Download file | ||||
| 					progressCh, err := client.Download(ctx, localPath, remotePath) | ||||
| 					if err != nil { | ||||
| 						guiErr(err, "Error downloading file", false, w) | ||||
| 						return | ||||
| 					} | ||||
|  | ||||
| 					// For every progress event | ||||
| 					for progressEvt := range progressCh { | ||||
| 						progressDlg.SetTotal(float64(progressEvt.Total)) | ||||
| 						progressDlg.SetValue(float64(progressEvt.Sent)) | ||||
| 					} | ||||
|  | ||||
| 					// Close progress dialog | ||||
| 					progressDlg.Hide() | ||||
| 				}, w) | ||||
| 				// Set filename to the item name | ||||
| 				dlg.SetFileName(item.Name) | ||||
| 				dlg.Show() | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if item.Name == ".." { | ||||
| 			out[index] = btn | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		moveBtn := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() { | ||||
| 			moveEntry := widget.NewEntry() | ||||
| 			dlg := dialog.NewForm("Move", "Move", "Cancel", []*widget.FormItem{ | ||||
| 				widget.NewFormItem("New Path", moveEntry), | ||||
| 			}, func(ok bool) { | ||||
| 				if !ok { | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				// Get current directory | ||||
| 				cwd, _ := cwdData.Get() | ||||
| 				// Join current directory with item name | ||||
| 				oldPath := filepath.Join(cwd, item.Name) | ||||
|  | ||||
| 				// Rename file | ||||
| 				err := client.Rename(ctx, oldPath, moveEntry.Text) | ||||
| 				if err != nil { | ||||
| 					guiErr(err, "Error renaming file", false, w) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				// Refresh GUI | ||||
| 				refresh(ctx, cwdData, lsData, client, w, c) | ||||
| 			}, w) | ||||
| 			dlg.Show() | ||||
| 		}) | ||||
|  | ||||
| 		removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { | ||||
| 			// Get current directory | ||||
| 			cwd, _ := cwdData.Get() | ||||
| 			// Join current directory with item name | ||||
| 			path := filepath.Join(cwd, item.Name) | ||||
|  | ||||
| 			// Remove file | ||||
| 			err := client.Remove(ctx, path) | ||||
| 			if err != nil { | ||||
| 				guiErr(err, "Error removing file", false, w) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// Refresh GUI | ||||
| 			refresh(ctx, cwdData, lsData, client, w, c) | ||||
| 		}) | ||||
|  | ||||
| 		// Add button to GUI component list | ||||
| 		out[index] = container.NewBorder( | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			container.NewHBox(moveBtn, removeBtn), | ||||
| 			btn, | ||||
| 		) | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
|  | ||||
| func refresh( | ||||
| 	ctx context.Context, | ||||
| 	cwdData binding.String, | ||||
| 	lsData binding.UntypedList, | ||||
| 	client *api.Client, | ||||
| 	w fyne.Window, | ||||
| 	c *fyne.Container, | ||||
| ) { | ||||
| 	// Create and show new loading pop up | ||||
| 	loading := newLoadingPopUp(w) | ||||
| 	loading.Show() | ||||
| 	// Close pop up at the end of the function | ||||
| 	defer loading.Hide() | ||||
|  | ||||
| 	// Get current directory | ||||
| 	cwd, _ := cwdData.Get() | ||||
| 	// Read directory | ||||
| 	ls, err := client.ReadDir(ctx, cwd) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error reading directory", false, w) | ||||
| 		return | ||||
| 	} | ||||
| 	// Set new listing data | ||||
| 	lsData.Set(lsToAny(ls)) | ||||
| 	// Create new GUI objects | ||||
| 	c.Objects = makeItems(ctx, client, lsData, cwdData, w, c) | ||||
| 	// Refresh GUI | ||||
| 	c.Refresh() | ||||
| } | ||||
|  | ||||
| func lsToAny(ls []api.FileInfo) []interface{} { | ||||
| 	out := make([]interface{}, len(ls)-1) | ||||
| 	for i, e := range ls { | ||||
| 		// Skip first element as it is always "." | ||||
| 		if i == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		out[i-1] = e | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
| @@ -3,122 +3,84 @@ package main | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"image/color" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/canvas" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/theme" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func infoTab(parent fyne.Window, client *api.Client) *fyne.Container { | ||||
| 	infoLayout := container.NewVBox( | ||||
| 		// Add rectangle for a bit of padding | ||||
| 		canvas.NewRectangle(color.Transparent), | ||||
| 	) | ||||
|  | ||||
| 	// Create label for heart rate | ||||
| 	heartRateLbl := newText("0 BPM", 24) | ||||
| 	// Creae container to store heart rate section | ||||
| 	heartRateSect := container.NewVBox( | ||||
| 		newText("Heart Rate", 12), | ||||
| 		heartRateLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	infoLayout.Add(heartRateSect) | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	onClose = append(onClose, cancel) | ||||
| func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject { | ||||
| 	c := container.NewVBox() | ||||
|  | ||||
| 	// Create titled text for heart rate | ||||
| 	heartRateText := newTitledText("Heart Rate", "0 BPM") | ||||
| 	c.Add(heartRateText) | ||||
| 	// Watch heart rate | ||||
| 	heartRateCh, err := client.WatchHeartRate(ctx) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error getting heart rate channel", true, parent) | ||||
| 		guiErr(err, "Error watching heart rate", true, w) | ||||
| 	} | ||||
| 	go func() { | ||||
| 		// For every heart rate sample | ||||
| 		for heartRate := range heartRateCh { | ||||
| 			// Change text of heart rate label | ||||
| 			heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate) | ||||
| 			// Refresh label | ||||
| 			heartRateLbl.Refresh() | ||||
| 			// Set body of titled text | ||||
| 			heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate)) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	// Create label for heart rate | ||||
| 	stepCountLbl := newText("0 Steps", 24) | ||||
| 	// Creae container to store heart rate section | ||||
| 	stepCountSect := container.NewVBox( | ||||
| 		newText("Step Count", 12), | ||||
| 		stepCountLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	infoLayout.Add(stepCountSect) | ||||
|  | ||||
| 	stepCountCh, err := client.WatchStepCount(ctx) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error getting step count channel", true, parent) | ||||
| 	} | ||||
| 	go func() { | ||||
| 		for stepCount := range stepCountCh { | ||||
| 			// Change text of heart rate label | ||||
| 			stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount) | ||||
| 			// Refresh label | ||||
| 			stepCountLbl.Refresh() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	// Create label for battery level | ||||
| 	battLevelLbl := newText("0%", 24) | ||||
| 	// Create container to store battery level section | ||||
| 	battLevel := container.NewVBox( | ||||
| 		newText("Battery Level", 12), | ||||
| 		battLevelLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	infoLayout.Add(battLevel) | ||||
|  | ||||
| 	// Create titled text for battery level | ||||
| 	battLevelText := newTitledText("Battery Level", "0%") | ||||
| 	c.Add(battLevelText) | ||||
| 	// Watch battery level | ||||
| 	battLevelCh, err := client.WatchBatteryLevel(ctx) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error getting battery level channel", true, parent) | ||||
| 		guiErr(err, "Error watching battery level", true, w) | ||||
| 	} | ||||
| 	go func() { | ||||
| 		// For every battery level sample | ||||
| 		for battLevel := range battLevelCh { | ||||
| 			// Change text of battery level label | ||||
| 			battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel) | ||||
| 			// Refresh label | ||||
| 			battLevelLbl.Refresh() | ||||
| 			// Set body of titled text | ||||
| 			battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel)) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	fwVerString, err := client.Version(context.Background()) | ||||
| 	// Create titled text for step count | ||||
| 	stepCountText := newTitledText("Step Count", "0 Steps") | ||||
| 	c.Add(stepCountText) | ||||
| 	// Watch step count | ||||
| 	stepCountCh, err := client.WatchStepCount(ctx) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error getting firmware string", true, parent) | ||||
| 		guiErr(err, "Error watching step count", true, w) | ||||
| 	} | ||||
| 	go func() { | ||||
| 		// For every step count sample | ||||
| 		for stepCount := range stepCountCh { | ||||
| 			// Set body of titled text | ||||
| 			stepCountText.SetBody(fmt.Sprintf("%d Steps", stepCount)) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	fwVer := container.NewVBox( | ||||
| 		newText("Firmware Version", 12), | ||||
| 		newText(fwVerString, 24), | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	infoLayout.Add(fwVer) | ||||
|  | ||||
| 	btAddrString, err := client.Address(context.Background()) | ||||
| 	// Create new titled text for address | ||||
| 	addressText := newTitledText("Address", "") | ||||
| 	c.Add(addressText) | ||||
| 	// Get address | ||||
| 	address, err := client.Address(ctx) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 		guiErr(err, "Error getting address", true, w) | ||||
| 	} | ||||
| 	// Set body of titled text | ||||
| 	addressText.SetBody(address) | ||||
|  | ||||
| 	btAddr := container.NewVBox( | ||||
| 		newText("Bluetooth Address", 12), | ||||
| 		newText(btAddrString, 24), | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	infoLayout.Add(btAddr) | ||||
| 	// Create new titled text for version | ||||
| 	versionText := newTitledText("Version", "") | ||||
| 	c.Add(versionText) | ||||
| 	// Get version | ||||
| 	version, err := client.Version(ctx) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error getting version", true, w) | ||||
| 	} | ||||
| 	// Set body of titled text | ||||
| 	versionText.SetBody(version) | ||||
|  | ||||
| 	return infoLayout | ||||
| } | ||||
|  | ||||
| func newText(t string, size float32) *canvas.Text { | ||||
| 	text := canvas.NewText(t, theme.ForegroundColor()) | ||||
| 	text.TextSize = size | ||||
| 	return text | ||||
| 	return container.NewVScroll(c) | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								cmd/itgui/loading.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"image/color" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/canvas" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| ) | ||||
|  | ||||
| func newLoadingPopUp(w fyne.Window) *widget.PopUp { | ||||
| 	pb := widget.NewProgressBarInfinite() | ||||
| 	rect := canvas.NewRectangle(color.Transparent) | ||||
| 	rect.SetMinSize(fyne.NewSize(200, 0)) | ||||
|  | ||||
| 	return widget.NewModalPopUp( | ||||
| 		container.NewMax(rect, pb), | ||||
| 		w.Canvas(), | ||||
| 	) | ||||
| } | ||||
| @@ -1,43 +1,55 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"sync" | ||||
|  | ||||
| 	"fyne.io/fyne/v2/app" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| var onClose []func() | ||||
|  | ||||
| func main() { | ||||
| 	// Create new app | ||||
| 	a := app.New() | ||||
| 	// Create new window with title "itgui" | ||||
| 	window := a.NewWindow("itgui") | ||||
| 	window.SetOnClosed(func() { | ||||
| 		for _, closeFn := range onClose { | ||||
| 			closeFn() | ||||
| 		} | ||||
| 	}) | ||||
| 	w := a.NewWindow("itgui") | ||||
|  | ||||
| 	// Create new context for use with the API client | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
|  | ||||
| 	// Connect to ITD API | ||||
| 	client, err := api.New(api.DefaultAddr) | ||||
| 	if err != nil { | ||||
| 		guiErr(err, "Error connecting to itd", true, window) | ||||
| 		guiErr(err, "Error connecting to ITD", true, w) | ||||
| 	} | ||||
| 	onClose = append(onClose, func() { | ||||
| 		client.Close() | ||||
| 	}) | ||||
|  | ||||
| 	// Create new app tabs container | ||||
| 	// Create channel to signal that the fs tab has been opened | ||||
| 	fsOpened := make(chan struct{}) | ||||
| 	fsOnce := &sync.Once{} | ||||
|  | ||||
| 	// Create app tabs | ||||
| 	tabs := container.NewAppTabs( | ||||
| 		container.NewTabItem("Info", infoTab(window, client)), | ||||
| 		container.NewTabItem("Motion", motionTab(window, client)), | ||||
| 		container.NewTabItem("Notify", notifyTab(window, client)), | ||||
| 		container.NewTabItem("Set Time", timeTab(window, client)), | ||||
| 		container.NewTabItem("Upgrade", upgradeTab(window, client)), | ||||
| 		container.NewTabItem("Info", infoTab(ctx, client, w)), | ||||
| 		container.NewTabItem("Motion", motionTab(ctx, client, w)), | ||||
| 		container.NewTabItem("Notify", notifyTab(ctx, client, w)), | ||||
| 		container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)), | ||||
| 		container.NewTabItem("Time", timeTab(ctx, client, w)), | ||||
| 		container.NewTabItem("Firmware", firmwareTab(ctx, client, w)), | ||||
| 	) | ||||
|  | ||||
| 	// Set tabs as window content | ||||
| 	window.SetContent(tabs) | ||||
| 	// Show window and run app | ||||
| 	window.ShowAndRun() | ||||
| 	// When a tab is selected | ||||
| 	tabs.OnSelected = func(ti *container.TabItem) { | ||||
| 		// If the tab's name is FS | ||||
| 		if ti.Text == "FS" { | ||||
| 			// Signal fsOpened only once | ||||
| 			fsOnce.Do(func() { | ||||
| 				fsOpened <- struct{}{} | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Cancel context on close | ||||
| 	w.SetOnClosed(cancel) | ||||
| 	// Set content and show window | ||||
| 	w.SetContent(tabs) | ||||
| 	w.ShowAndRun() | ||||
| } | ||||
|   | ||||
| @@ -2,109 +2,61 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"image/color" | ||||
| 	"strconv" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/canvas" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/theme" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func motionTab(parent fyne.Window, client *api.Client) *fyne.Container { | ||||
| 	// Create label for heart rate | ||||
| 	xCoordLbl := newText("0", 24) | ||||
| 	// Creae container to store heart rate section | ||||
| 	xCoordSect := container.NewVBox( | ||||
| 		newText("X Coordinate", 12), | ||||
| 		xCoordLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject { | ||||
| 	// Create titledText for each coordinate | ||||
| 	xText := newTitledText("X Coordinate", "0") | ||||
| 	yText := newTitledText("Y Coordinate", "0") | ||||
| 	zText := newTitledText("Z Coordinate", "0") | ||||
|  | ||||
| 	// Create label for heart rate | ||||
| 	yCoordLbl := newText("0", 24) | ||||
| 	// Creae container to store heart rate section | ||||
| 	yCoordSect := container.NewVBox( | ||||
| 		newText("Y Coordinate", 12), | ||||
| 		yCoordLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	// Create label for heart rate | ||||
| 	zCoordLbl := newText("0", 24) | ||||
| 	// Creae container to store heart rate section | ||||
| 	zCoordSect := container.NewVBox( | ||||
| 		newText("Z Coordinate", 12), | ||||
| 		zCoordLbl, | ||||
| 		canvas.NewLine(theme.ShadowColor()), | ||||
| 	) | ||||
| 	var ctxCancel func() | ||||
|  | ||||
| 	// Create variable to keep track of whether motion started | ||||
| 	started := false | ||||
|  | ||||
| 	// Create button to stop motion | ||||
| 	stopBtn := widget.NewButton("Stop", nil) | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	onClose = append(onClose, cancel) | ||||
|  | ||||
| 	// Create button to start motion | ||||
| 	startBtn := widget.NewButton("Start", func() { | ||||
| 		// if motion is started | ||||
| 		if started { | ||||
| 			// Do nothing | ||||
| 			return | ||||
| 		} | ||||
| 		// Set motion started | ||||
| 		started = true | ||||
| 		// Watch motion values | ||||
| 		motionCh, err := client.WatchMotion(ctx) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error getting heart rate channel", true, parent) | ||||
| 		} | ||||
| 		// Create done channel | ||||
| 		done := make(chan struct{}, 1) | ||||
| 		go func() { | ||||
| 			for { | ||||
| 				select { | ||||
| 				case <-done: | ||||
| 					return | ||||
| 				case motion := <-motionCh: | ||||
| 					// Set labels to new values | ||||
| 					xCoordLbl.Text = strconv.Itoa(int(motion.X)) | ||||
| 					yCoordLbl.Text = strconv.Itoa(int(motion.Y)) | ||||
| 					zCoordLbl.Text = strconv.Itoa(int(motion.Z)) | ||||
| 					// Refresh labels to display new values | ||||
| 					xCoordLbl.Refresh() | ||||
| 					yCoordLbl.Refresh() | ||||
| 					zCoordLbl.Refresh() | ||||
| 				} | ||||
| 	// Create start button | ||||
| 	toggleBtn := widget.NewButton("Start", nil) | ||||
| 	// Set button's on tapped callback | ||||
| 	toggleBtn.OnTapped = func() { | ||||
| 		switch toggleBtn.Text { | ||||
| 		case "Start": | ||||
| 			// Create new context for motion | ||||
| 			motionCtx, cancel := context.WithCancel(ctx) | ||||
| 			// Set ctxCancel to function so that stop button can run it | ||||
| 			ctxCancel = cancel | ||||
| 			// Watch motion | ||||
| 			motionCh, err := client.WatchMotion(motionCtx) | ||||
| 			if err != nil { | ||||
| 				guiErr(err, "Error watching motion", false, w) | ||||
| 				return | ||||
| 			} | ||||
| 		}() | ||||
| 		// Create stop function | ||||
| 		stopBtn.OnTapped = func() { | ||||
| 			done <- struct{}{} | ||||
| 			started = false | ||||
| 			cancel() | ||||
| 			go func() { | ||||
| 				// For every motion event | ||||
| 				for motion := range motionCh { | ||||
| 					// Set coordinates | ||||
| 					xText.SetBody(fmt.Sprint(motion.X)) | ||||
| 					yText.SetBody(fmt.Sprint(motion.Y)) | ||||
| 					zText.SetBody(fmt.Sprint(motion.Z)) | ||||
| 				} | ||||
| 			}() | ||||
| 			// Set button text to "Stop" | ||||
| 			toggleBtn.SetText("Stop") | ||||
| 		case "Stop": | ||||
| 			// Cancel motion context | ||||
| 			ctxCancel() | ||||
| 			// Set button text to "Start" | ||||
| 			toggleBtn.SetText("Start") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	}) | ||||
| 	// Run stop button function on close if possible | ||||
| 	onClose = append(onClose, func() { | ||||
| 		if stopBtn.OnTapped != nil { | ||||
| 			stopBtn.OnTapped() | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	// Return new container containing all elements | ||||
| 	return container.NewVBox( | ||||
| 		// Add rectangle for a bit of padding | ||||
| 		canvas.NewRectangle(color.Transparent), | ||||
| 		startBtn, | ||||
| 		stopBtn, | ||||
| 		xCoordSect, | ||||
| 		yCoordSect, | ||||
| 		zCoordSect, | ||||
| 	) | ||||
| 	return container.NewVScroll(container.NewVBox( | ||||
| 		toggleBtn, | ||||
| 		xText, | ||||
| 		yText, | ||||
| 		zText, | ||||
| 	)) | ||||
| } | ||||
|   | ||||
| @@ -10,30 +10,31 @@ import ( | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container { | ||||
| 	// Create new entry for notification title | ||||
| func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject { | ||||
| 	c := container.NewVBox() | ||||
| 	c.Add(layout.NewSpacer()) | ||||
|  | ||||
| 	// Create new entry for title | ||||
| 	titleEntry := widget.NewEntry() | ||||
| 	titleEntry.SetPlaceHolder("Title") | ||||
| 	c.Add(titleEntry) | ||||
|  | ||||
| 	// Create multiline entry for notification body | ||||
| 	// Create new multiline entry for body | ||||
| 	bodyEntry := widget.NewMultiLineEntry() | ||||
| 	bodyEntry.SetPlaceHolder("Body") | ||||
| 	c.Add(bodyEntry) | ||||
|  | ||||
| 	// Create new button to send notification | ||||
| 	// Create new send button | ||||
| 	sendBtn := widget.NewButton("Send", func() { | ||||
| 		err := client.Notify(context.Background(), titleEntry.Text, bodyEntry.Text) | ||||
| 		// Send notification | ||||
| 		err := client.Notify(ctx, titleEntry.Text, bodyEntry.Text) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error sending notification", false, parent) | ||||
| 			guiErr(err, "Error sending notification", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 	}) | ||||
| 	c.Add(sendBtn) | ||||
|  | ||||
| 	// Return new container containing all elements | ||||
| 	return container.NewVBox( | ||||
| 		layout.NewSpacer(), | ||||
| 		titleEntry, | ||||
| 		bodyEntry, | ||||
| 		sendBtn, | ||||
| 		layout.NewSpacer(), | ||||
| 	) | ||||
| 	c.Add(layout.NewSpacer()) | ||||
| 	return container.NewVScroll(c) | ||||
| } | ||||
|   | ||||
							
								
								
									
										50
									
								
								cmd/itgui/progress.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image/color" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/canvas" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| ) | ||||
|  | ||||
| type progress struct { | ||||
| 	lbl *widget.Label | ||||
| 	pb  *widget.ProgressBar | ||||
| 	*widget.PopUp | ||||
| } | ||||
|  | ||||
| func newProgress(w fyne.Window) progress { | ||||
| 	out := progress{} | ||||
|  | ||||
| 	// Create label to show how many bytes transfered and center it | ||||
| 	out.lbl = widget.NewLabel("0 / 0 B") | ||||
| 	out.lbl.Alignment = fyne.TextAlignCenter | ||||
|  | ||||
| 	// Create new progress bar | ||||
| 	out.pb = widget.NewProgressBar() | ||||
|  | ||||
| 	// Create new rectangle to set the size of the popup | ||||
| 	sizeRect := canvas.NewRectangle(color.Transparent) | ||||
| 	sizeRect.SetMinSize(fyne.NewSize(300, 50)) | ||||
| 	 | ||||
| 	// Create vbox for label and progress bar | ||||
| 	l := container.NewVBox(out.lbl, out.pb) | ||||
| 	// Create popup | ||||
| 	out.PopUp = widget.NewModalPopUp(container.NewMax(l, sizeRect), w.Canvas()) | ||||
|  | ||||
| 	return out | ||||
| } | ||||
|  | ||||
| func (p progress) SetTotal(v float64) { | ||||
| 	p.pb.Max = v | ||||
| 	p.pb.Refresh() | ||||
| 	p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", p.pb.Value, v)) | ||||
| } | ||||
|  | ||||
| func (p progress) SetValue(v float64) { | ||||
| 	p.pb.SetValue(v) | ||||
| 	p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", v, p.pb.Max)) | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/firmware.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/fs.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/info.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/mkdir.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/motion.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/notify.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/progress.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								cmd/itgui/screenshots/time.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
| @@ -11,51 +11,47 @@ import ( | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func timeTab(parent fyne.Window, client *api.Client) *fyne.Container { | ||||
| 	// Create new entry for time string | ||||
| func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject { | ||||
| 	c := container.NewVBox() | ||||
| 	c.Add(layout.NewSpacer()) | ||||
|  | ||||
| 	// Create entry for time string | ||||
| 	timeEntry := widget.NewEntry() | ||||
| 	// Set text to current time formatter properly | ||||
| 	timeEntry.SetText(time.Now().Format(time.RFC1123)) | ||||
| 	timeEntry.SetPlaceHolder("RFC1123") | ||||
|  | ||||
| 	// Create button to set current time | ||||
| 	currentBtn := widget.NewButton("Set Current", func() { | ||||
| 		timeEntry.SetText(time.Now().Format(time.RFC1123)) | ||||
| 		setTime(client, true) | ||||
| 	}) | ||||
|  | ||||
| 	// Create button to set time inside entry | ||||
| 	timeBtn := widget.NewButton("Set", func() { | ||||
| 		// Parse time as RFC1123 string | ||||
| 		parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text) | ||||
| 	setCurrentBtn := widget.NewButton("Set current time", func() { | ||||
| 		// Set current time | ||||
| 		err := client.SetTime(ctx, time.Now()) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error parsing time string", false, parent) | ||||
| 			guiErr(err, "Error setting time", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 		// Set time to parsed time | ||||
| 		setTime(client, false, parsedTime) | ||||
| 		// Set time entry to current time | ||||
| 		timeEntry.SetText(time.Now().Format(time.RFC1123)) | ||||
| 	}) | ||||
|  | ||||
| 	// Return new container with all elements centered | ||||
| 	return container.NewVBox( | ||||
| 		layout.NewSpacer(), | ||||
| 		timeEntry, | ||||
| 		currentBtn, | ||||
| 		timeBtn, | ||||
| 		layout.NewSpacer(), | ||||
| 	) | ||||
| } | ||||
| 	// Create button to set time from entry | ||||
| 	setBtn := widget.NewButton("Set", func() { | ||||
| 		// Parse RFC1123 time string in entry | ||||
| 		newTime, err := time.Parse(time.RFC1123, timeEntry.Text) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error parsing time string", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 		// Set time from parsed string | ||||
| 		err = client.SetTime(ctx, newTime) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error setting time", false, w) | ||||
| 			return | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| // setTime sets the first element in the variadic parameter | ||||
| // if current is false, otherwise, it sets the current time. | ||||
| func setTime(client *api.Client, current bool, t ...time.Time) error { | ||||
| 	var err error | ||||
| 	if current { | ||||
| 		err = client.SetTime(context.Background(), time.Now()) | ||||
| 	} else { | ||||
| 		err = client.SetTime(context.Background(), t[0]) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| 	c.Add(timeEntry) | ||||
| 	c.Add(setBtn) | ||||
| 	c.Add(setCurrentBtn) | ||||
|  | ||||
| 	c.Add(layout.NewSpacer()) | ||||
| 	return c | ||||
| } | ||||
|   | ||||
							
								
								
									
										35
									
								
								cmd/itgui/titledText.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| package main | ||||
|  | ||||
| import "fyne.io/fyne/v2/widget" | ||||
|  | ||||
| type titledText struct { | ||||
| 	*widget.RichText | ||||
| } | ||||
|  | ||||
| func newTitledText(title, text string) titledText { | ||||
| 	titleStyle := widget.RichTextStyleHeading | ||||
| 	titleStyle.TextStyle.Bold = false | ||||
| 	return titledText{ | ||||
| 		widget.NewRichText( | ||||
| 			&widget.TextSegment{ | ||||
| 				Style: widget.RichTextStyleParagraph, | ||||
| 				Text:  title, | ||||
| 			}, | ||||
| 			&widget.TextSegment{ | ||||
| 				Style: titleStyle, | ||||
| 				Text:  text, | ||||
| 			}, | ||||
| 			&widget.SeparatorSegment{}, | ||||
| 		), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t titledText) SetTitle(s string) { | ||||
| 	t.RichText.Segments[0].(*widget.TextSegment).Text = s | ||||
| 	t.Refresh() | ||||
| } | ||||
|  | ||||
| func (t titledText) SetBody(s string) { | ||||
| 	t.RichText.Segments[1].(*widget.TextSegment).Text = s | ||||
| 	t.Refresh() | ||||
| } | ||||
| @@ -1,181 +0,0 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"fyne.io/fyne/v2" | ||||
| 	"fyne.io/fyne/v2/container" | ||||
| 	"fyne.io/fyne/v2/dialog" | ||||
| 	"fyne.io/fyne/v2/layout" | ||||
| 	"fyne.io/fyne/v2/storage" | ||||
| 	"fyne.io/fyne/v2/widget" | ||||
| 	"go.arsenm.dev/itd/api" | ||||
| ) | ||||
|  | ||||
| func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container { | ||||
| 	var ( | ||||
| 		archivePath  string | ||||
| 		firmwarePath string | ||||
| 		initPktPath  string | ||||
| 	) | ||||
|  | ||||
| 	var archiveBtn *widget.Button | ||||
| 	// Create archive selection dialog | ||||
| 	archiveDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) { | ||||
| 		if e != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		uc.Close() | ||||
| 		archivePath = uc.URI().Path() | ||||
| 		archiveBtn.SetText(fmt.Sprintf("Select archive (.zip) [%s]", filepath.Base(archivePath))) | ||||
| 	}, parent) | ||||
| 	// Limit dialog to .zip files | ||||
| 	archiveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".zip"})) | ||||
| 	// Create button to show dialog | ||||
| 	archiveBtn = widget.NewButton("Select archive (.zip)", archiveDialog.Show) | ||||
|  | ||||
| 	var firmwareBtn *widget.Button | ||||
| 	// Create firmware selection dialog | ||||
| 	firmwareDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) { | ||||
| 		if e != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		uc.Close() | ||||
| 		firmwarePath = uc.URI().Path() | ||||
| 		firmwareBtn.SetText(fmt.Sprintf("Select firmware (.bin) [%s]", filepath.Base(firmwarePath))) | ||||
| 	}, parent) | ||||
| 	// Limit dialog to .bin files | ||||
| 	firmwareDialog.SetFilter(storage.NewExtensionFileFilter([]string{".bin"})) | ||||
| 	// Create button to show dialog | ||||
| 	firmwareBtn = widget.NewButton("Select firmware (.bin)", firmwareDialog.Show) | ||||
|  | ||||
| 	var initPktBtn *widget.Button | ||||
| 	// Create init packet selection dialog | ||||
| 	initPktDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) { | ||||
| 		if e != nil || uc == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		uc.Close() | ||||
| 		initPktPath = uc.URI().Path() | ||||
| 		initPktBtn.SetText(fmt.Sprintf("Select init packet (.dat) [%s]", filepath.Base(initPktPath))) | ||||
| 	}, parent) | ||||
| 	// Limit dialog to .dat files | ||||
| 	initPktDialog.SetFilter(storage.NewExtensionFileFilter([]string{".dat"})) | ||||
| 	// Create button to show dialog | ||||
| 	initPktBtn = widget.NewButton("Select init packet (.dat)", initPktDialog.Show) | ||||
|  | ||||
| 	// Hide init packet and firmware buttons | ||||
| 	initPktBtn.Hide() | ||||
| 	firmwareBtn.Hide() | ||||
|  | ||||
| 	// Create dropdown to select upgrade type | ||||
| 	upgradeTypeSelect := widget.NewSelect([]string{ | ||||
| 		"Archive", | ||||
| 		"Files", | ||||
| 	}, func(s string) { | ||||
| 		// Hide all buttons | ||||
| 		archiveBtn.Hide() | ||||
| 		initPktBtn.Hide() | ||||
| 		firmwareBtn.Hide() | ||||
| 		// Unhide appropriate button(s) | ||||
| 		switch s { | ||||
| 		case "Archive": | ||||
| 			archiveBtn.Show() | ||||
| 		case "Files": | ||||
| 			initPktBtn.Show() | ||||
| 			firmwareBtn.Show() | ||||
| 		} | ||||
| 	}) | ||||
| 	// Select first elemetn | ||||
| 	upgradeTypeSelect.SetSelectedIndex(0) | ||||
|  | ||||
| 	// Create new button to start DFU | ||||
| 	startBtn := widget.NewButton("Start", func() { | ||||
| 		// If archive path does not exist and both init packet and firmware paths | ||||
| 		// also do not exist, return error | ||||
| 		if archivePath == "" && (initPktPath == "" && firmwarePath == "") { | ||||
| 			guiErr(nil, "Upgrade requires archive or files selected", false, parent) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Create new label for byte progress | ||||
| 		progressLbl := widget.NewLabelWithStyle("0 / 0 B", fyne.TextAlignCenter, fyne.TextStyle{}) | ||||
| 		// Create new progress bar | ||||
| 		progressBar := widget.NewProgressBar() | ||||
| 		// Create modal dialog containing label and progress bar | ||||
| 		progressDlg := widget.NewModalPopUp(container.NewVBox( | ||||
| 			layout.NewSpacer(), | ||||
| 			progressLbl, | ||||
| 			progressBar, | ||||
| 			layout.NewSpacer(), | ||||
| 		), parent.Canvas()) | ||||
| 		// Resize modal to 300x100 | ||||
| 		progressDlg.Resize(fyne.NewSize(300, 100)) | ||||
|  | ||||
| 		var fwUpgType api.UpgradeType | ||||
| 		var files []string | ||||
| 		// Get appropriate upgrade type and file paths | ||||
| 		switch upgradeTypeSelect.Selected { | ||||
| 		case "Archive": | ||||
| 			fwUpgType = api.UpgradeTypeArchive | ||||
| 			files = append(files, archivePath) | ||||
| 		case "Files": | ||||
| 			fwUpgType = api.UpgradeTypeFiles | ||||
| 			files = append(files, initPktPath, firmwarePath) | ||||
| 		} | ||||
|  | ||||
| 		progress, err := client.FirmwareUpgrade(context.Background(), fwUpgType, files...) | ||||
| 		if err != nil { | ||||
| 			guiErr(err, "Error initiating DFU", false, parent) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Show progress dialog | ||||
| 		progressDlg.Show() | ||||
|  | ||||
| 		for event := range progress { | ||||
| 			// Set label text to received / total B | ||||
| 			progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total)) | ||||
| 			// Set progress bar values | ||||
| 			progressBar.Max = float64(event.Total) | ||||
| 			progressBar.Value = float64(event.Received) | ||||
| 			// Refresh progress bar | ||||
| 			progressBar.Refresh() | ||||
| 			// If transfer finished, break | ||||
| 			if int64(event.Sent) == event.Total { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Hide progress dialog after completion | ||||
| 		progressDlg.Hide() | ||||
|  | ||||
| 		// Reset screen to default | ||||
| 		upgradeTypeSelect.SetSelectedIndex(0) | ||||
| 		firmwareBtn.SetText("Select firmware (.bin)") | ||||
| 		initPktBtn.SetText("Select init packet (.dat)") | ||||
| 		archiveBtn.SetText("Select archive (.zip)") | ||||
| 		firmwarePath = "" | ||||
| 		initPktPath = "" | ||||
| 		archivePath = "" | ||||
|  | ||||
| 		dialog.NewInformation( | ||||
| 			"Upgrade Complete", | ||||
| 			"The firmware was transferred successfully.\nRemember to validate the firmware in InfiniTime settings.", | ||||
| 			parent, | ||||
| 		).Show() | ||||
| 	}) | ||||
|  | ||||
| 	// Return container containing all elements | ||||
| 	return container.NewVBox( | ||||
| 		layout.NewSpacer(), | ||||
| 		upgradeTypeSelect, | ||||
| 		archiveBtn, | ||||
| 		firmwareBtn, | ||||
| 		initPktBtn, | ||||
| 		startBtn, | ||||
| 		layout.NewSpacer(), | ||||
| 	) | ||||
| } | ||||