package main import ( "image/color" "log" "path/filepath" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "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" ) type FlashKind int const ( FlashError FlashKind = iota FlashSuccess ) var ( presFileFilter []string = []string{".pptx", ".odp"} videoFileFilter []string = []string{".mp4", ".mkv", ".mov", ".webm"} flashTimers = map[*widget.Button]*time.Timer{} ) func flashColor(btn *widget.Button, kind FlashKind) { if t, ok := flashTimers[btn]; ok { t.Stop() // cancel previous flash } duration := 1 * time.Second // default flash duration // Determine button style based on kind var importance widget.Importance switch kind { case FlashError: importance = widget.DangerImportance case FlashSuccess: importance = widget.SuccessImportance duration = 3 * time.Second } // Apply the flash immediately on UI thread fyne.CurrentApp().Driver().DoFromGoroutine(func() { btn.Importance = importance btn.Refresh() }, false) // Schedule reset after duration (non-blocking) flashTimers[btn] = time.AfterFunc(duration, func() { fyne.CurrentApp().Driver().DoFromGoroutine(func() { btn.Importance = widget.LowImportance btn.Refresh() }, false) }) } func drawSeparator(trailNewLine bool) *fyne.Container { emptyLabel := widget.NewLabel("") // Give it some space at the bottom with an empty line separatorLine := canvas.NewLine(color.Gray{Y: 128}) var separContainer *fyne.Container if trailNewLine { separContainer = container.NewVBox( emptyLabel, separatorLine, emptyLabel, ) } else { separContainer = container.NewVBox( emptyLabel, separatorLine, ) } return separContainer } func drawModeRow(targetMode *int) *fyne.Container { actionText := widget.NewLabel("Select Mode") // targetMode is defined as: // 0 undefined / not used // 1 presentation (PowerPoint, Impress) // 2 video (mp4, mkv) var presModeBtn *widget.Button var videoModeBtn *widget.Button presModeBtn = widget.NewButton("Presentation", func() { *targetMode = 1 presModeBtn.Importance = widget.HighImportance videoModeBtn.Importance = widget.LowImportance log.Println("Set mode to presentation", *targetMode) refreshButtons(presModeBtn, videoModeBtn) }) presWide := container.NewGridWrap( buttonSize, presModeBtn, ) videoModeBtn = widget.NewButton("Video", func() { *targetMode = 2 presModeBtn.Importance = widget.LowImportance videoModeBtn.Importance = widget.HighImportance log.Println("Set mode to video", *targetMode) refreshButtons(presModeBtn, videoModeBtn) }) videoWide := container.NewGridWrap( buttonSize, videoModeBtn, ) modeRow := container.NewHBox( presWide, widget.NewLabel(""), videoWide, ) modeCol := container.NewVBox( actionText, modeRow, ) return modeCol } func drawTargetSection(raspiNames []string, raspiTarget *string, uploadBtn, reloadBtn *widget.Button, cfg RaspiConfig) *fyne.Container { actionText := widget.NewLabel("Select Target") var previousTarget string var verifyBtn *widget.Button // Left side for selection of target piSelection := widget.NewRadioGroup(raspiNames, func(selected string) { if selected == previousTarget { return // nothing changed } previousTarget = selected *raspiTarget = selected refreshButtons(verifyBtn, uploadBtn, reloadBtn) }) selecCol := container.NewVBox( actionText, piSelection, ) var credOK bool verifyBtn = widget.NewButton("Check Connection", func() { verifyBtn.Importance = widget.HighImportance refreshButtons(verifyBtn) log.Println("Verifying credentials...") uploadBtn.Disable() reloadBtn.Disable() refreshButtons(verifyBtn, uploadBtn, reloadBtn) go func() { credOK = verifyCred(*raspiTarget, cfg) if credOK { // Must update UI on main thread fyne.CurrentApp().Driver().DoFromGoroutine(func() { flashColor(verifyBtn, FlashSuccess) // flashcolor should handle RunOnMain internally uploadBtn.Enable() reloadBtn.Enable() }, false) } else { flashColor(verifyBtn, FlashError) // flashcolor should handle RunOnMain internally } }() }) verifyWide := container.NewGridWrap( buttonSize, verifyBtn, ) wholeCol := container.NewVBox( selecCol, widget.NewLabel(""), verifyWide, ) return wholeCol } func drawFileSelection(localPath *string, targetMode *int, parentWindow fyne.Window) *fyne.Container { actionText := widget.NewLabel("Select File") pathLabel := widget.NewLabel("No File Selected Yet...") var uploadBtn *widget.Button uploadBtn = widget.NewButton("Click to Select File", func() { // Stupid dialog error on 'headless' servers... log.Println("Ignore the next error. ~Refer: https://github.com/fyne-io/fyne/issues/4110") // Use NewFileOpen to get the dialog object uploadSelecDiag := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if r != nil { defer r.Close() pathLabel.SetText(r.URI().Name()) *localPath = r.URI().Path() uploadBtn.Importance = widget.HighImportance refreshButtons(uploadBtn) log.Println("Local path of selected file:", *localPath) } }, parentWindow) var fileFilter storage.FileFilter switch *targetMode { case 1: fileFilter = storage.NewExtensionFileFilter(presFileFilter) case 2: fileFilter = storage.NewExtensionFileFilter(videoFileFilter) } uploadSelecDiag.SetFilter(fileFilter) // Resize the dialog to the whole window, if it wants to do that - uploadSelecDiag.Resize(windowSize) uploadSelecDiag.Show() }) uploadWide := container.NewGridWrap( buttonSize, uploadBtn, ) fileSelecRow := container.NewHBox( uploadWide, pathLabel, ) fileSelecCol := container.NewVBox( actionText, fileSelecRow, ) return fileSelecCol } // targetMode *int func drawFooter(app fyne.App, raspiTarget, localUploadPath *string, targetMode *int, cfg RaspiConfig) (*fyne.Container, *widget.Button, *widget.Button) { fExt := filepath.Ext(*localUploadPath) remotePresPath := "/opt/akartontv/media" + fExt // Configuration of the bottom of the application var uploadBtn *widget.Button uploadBtn = widget.NewButton("Upload File", func() { uploadBtn.Importance = widget.HighImportance refreshButtons(uploadBtn) go func() { ok := sftpUploadFile(*raspiTarget, *localUploadPath, remotePresPath, cfg) if ok { flashColor(uploadBtn, FlashSuccess) } else { flashColor(uploadBtn, FlashError) } }() }) uploadWide := container.NewGridWrap( dButtonSize, uploadBtn, ) var reloadBtn *widget.Button reloadBtn = widget.NewButton("Restart Program", func() { reloadBtn.Importance = widget.HighImportance refreshButtons(uploadBtn) go func() { ok := restartShow(*raspiTarget, *targetMode, cfg) if ok { flashColor(reloadBtn, FlashSuccess) } else { flashColor(reloadBtn, FlashError) } }() }) reloadWide := container.NewGridWrap( dButtonSize, reloadBtn, ) cancelBtn := widget.NewButton("Exit", func() { log.Println("Quitting") app.Quit() }) cancelWide := container.NewGridWrap( buttonSize, cancelBtn, ) bottom := container.NewHBox( uploadWide, // left reloadWide, // also next to the one above layout.NewSpacer(), // flexible space cancelWide, // right ) uploadBtn.Disable() reloadBtn.Disable() return bottom, uploadBtn, reloadBtn }