package player import ( "fmt" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" ) // FynePlayerUI provides a Fyne-based user interface for the VTPlayer type FynePlayerUI struct { app fyne.App window fyne.Window player VTPlayer container *fyne.Container // UI Components playPauseBtn *widget.Button stopBtn *widget.Button seekSlider *widget.Slider timeLabel *widget.Label durationLabel *widget.Label volumeSlider *widget.Slider fullscreenBtn *widget.Button fileBtn *widget.Button frameLabel *widget.Label fpsLabel *widget.Label // State tracking isPlaying bool currentTime time.Duration duration time.Duration manualSeek bool } // NewFynePlayerUI creates a new Fyne UI for the VTPlayer func NewFynePlayerUI(app fyne.App, player VTPlayer) *FynePlayerUI { ui := &FynePlayerUI{ app: app, player: player, window: app.NewWindow("VideoTools Player"), } ui.setupUI() ui.setupCallbacks() ui.setupWindow() return ui } // setupUI creates the user interface components func (ui *FynePlayerUI) setupUI() { // Control buttons - using text instead of icons for compatibility ui.playPauseBtn = widget.NewButton("Play", ui.togglePlayPause) ui.stopBtn = widget.NewButton("Stop", ui.stop) ui.fullscreenBtn = widget.NewButton("Fullscreen", ui.toggleFullscreen) ui.fileBtn = widget.NewButton("Open File", ui.openFile) // Time controls ui.seekSlider = widget.NewSlider(0, 100) ui.seekSlider.OnChanged = ui.onSeekChanged ui.timeLabel = widget.NewLabel("00:00:00") ui.durationLabel = widget.NewLabel("00:00:00") // Volume control ui.volumeSlider = widget.NewSlider(0, 100) ui.volumeSlider.SetValue(ui.player.GetVolume()) ui.volumeSlider.OnChanged = ui.onVolumeChanged // Info labels ui.frameLabel = widget.NewLabel("Frame: 0") ui.fpsLabel = widget.NewLabel("FPS: 0.0") // Volume percentage label volumeLabel := widget.NewLabel(fmt.Sprintf("%.0f%%", ui.player.GetVolume())) // Layout containers buttonContainer := container.NewHBox( ui.fileBtn, ui.playPauseBtn, ui.stopBtn, ui.fullscreenBtn, ) timeContainer := container.NewHBox( ui.timeLabel, ui.seekSlider, ui.durationLabel, ) volumeContainer := container.NewHBox( widget.NewLabel("Volume:"), ui.volumeSlider, volumeLabel, ) infoContainer := container.NewHBox( ui.frameLabel, ui.fpsLabel, ) // Update volume label when slider changes ui.volumeSlider.OnChanged = func(value float64) { volumeLabel.SetText(fmt.Sprintf("%.0f%%", value)) ui.onVolumeChanged(value) } // Main container ui.container = container.NewVBox( buttonContainer, timeContainer, volumeContainer, infoContainer, ) } // setupCallbacks registers player event callbacks func (ui *FynePlayerUI) setupCallbacks() { ui.player.SetTimeCallback(ui.onTimeUpdate) ui.player.SetFrameCallback(ui.onFrameUpdate) ui.player.SetStateCallback(ui.onStateUpdate) } // setupWindow configures the main window func (ui *FynePlayerUI) setupWindow() { ui.window.SetContent(ui.container) ui.window.Resize(fyne.NewSize(600, 200)) ui.window.SetFixedSize(false) ui.window.CenterOnScreen() } // Show makes the player UI visible func (ui *FynePlayerUI) Show() { ui.window.Show() } // Hide makes the player UI invisible func (ui *FynePlayerUI) Hide() { ui.window.Hide() } // Close closes the player and UI func (ui *FynePlayerUI) Close() { ui.player.Close() ui.window.Close() } // togglePlayPause toggles between play and pause states func (ui *FynePlayerUI) togglePlayPause() { if ui.isPlaying { ui.pause() } else { ui.play() } } // play starts playback func (ui *FynePlayerUI) play() { if err := ui.player.Play(); err != nil { dialog.ShowError(fmt.Errorf("Failed to play: %w", err), ui.window) return } ui.isPlaying = true ui.playPauseBtn.SetText("Pause") } // pause pauses playback func (ui *FynePlayerUI) pause() { if err := ui.player.Pause(); err != nil { dialog.ShowError(fmt.Errorf("Failed to pause: %w", err), ui.window) return } ui.isPlaying = false ui.playPauseBtn.SetText("Play") } // stop stops playback func (ui *FynePlayerUI) stop() { if err := ui.player.Stop(); err != nil { dialog.ShowError(fmt.Errorf("Failed to stop: %w", err), ui.window) return } ui.isPlaying = false ui.playPauseBtn.SetText("Play") ui.seekSlider.SetValue(0) ui.timeLabel.SetText("00:00:00") } // toggleFullscreen toggles fullscreen mode func (ui *FynePlayerUI) toggleFullscreen() { // Note: This would need to be implemented per-backend // For now, just toggle the window fullscreen state ui.window.SetFullScreen(!ui.window.FullScreen()) } // openFile shows a file picker and loads the selected video func (ui *FynePlayerUI) openFile() { dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil || reader == nil { return } defer reader.Close() filePath := reader.URI().Path() if err := ui.player.Load(filePath, 0); err != nil { dialog.ShowError(fmt.Errorf("Failed to load file: %w", err), ui.window) return } // Update duration when file loads ui.duration = ui.player.GetDuration() ui.durationLabel.SetText(formatDuration(ui.duration)) ui.seekSlider.Max = float64(ui.duration.Milliseconds()) // Update video info info := ui.player.GetVideoInfo() ui.fpsLabel.SetText(fmt.Sprintf("FPS: %.2f", info.FrameRate)) }, ui.window) } // onSeekChanged handles seek slider changes func (ui *FynePlayerUI) onSeekChanged(value float64) { if ui.manualSeek { // Convert slider value to time duration seekTime := time.Duration(value) * time.Millisecond if err := ui.player.SeekToTime(seekTime); err != nil { dialog.ShowError(fmt.Errorf("Failed to seek: %w", err), ui.window) } } } // onVolumeChanged handles volume slider changes func (ui *FynePlayerUI) onVolumeChanged(value float64) { if err := ui.player.SetVolume(value); err != nil { dialog.ShowError(fmt.Errorf("Failed to set volume: %w", err), ui.window) return } } // onTimeUpdate handles time position updates from the player func (ui *FynePlayerUI) onTimeUpdate(currentTime time.Duration) { ui.currentTime = currentTime ui.timeLabel.SetText(formatDuration(currentTime)) // Update seek slider without triggering manual seek ui.manualSeek = false ui.seekSlider.SetValue(float64(currentTime.Milliseconds())) ui.manualSeek = true } // onFrameUpdate handles frame position updates from the player func (ui *FynePlayerUI) onFrameUpdate(frame int64) { ui.frameLabel.SetText(fmt.Sprintf("Frame: %d", frame)) } // onStateUpdate handles player state changes func (ui *FynePlayerUI) onStateUpdate(state PlayerState) { switch state { case StatePlaying: ui.isPlaying = true ui.playPauseBtn.SetText("Pause") case StatePaused: ui.isPlaying = false ui.playPauseBtn.SetText("Play") case StateStopped: ui.isPlaying = false ui.playPauseBtn.SetText("Play") ui.seekSlider.SetValue(0) ui.timeLabel.SetText("00:00:00") case StateError: ui.isPlaying = false ui.playPauseBtn.SetText("Play") dialog.ShowError(fmt.Errorf("Player error occurred"), ui.window) } } // formatDuration formats a time.Duration as HH:MM:SS func formatDuration(d time.Duration) string { if d < 0 { d = 0 } hours := int(d.Hours()) minutes := int(d.Minutes()) % 60 seconds := int(d.Seconds()) % 60 return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) } // LoadVideoFile loads a specific video file func (ui *FynePlayerUI) LoadVideoFile(filePath string, offset time.Duration) error { if err := ui.player.Load(filePath, offset); err != nil { return fmt.Errorf("failed to load video file: %w", err) } // Update duration when file loads ui.duration = ui.player.GetDuration() ui.durationLabel.SetText(formatDuration(ui.duration)) ui.seekSlider.Max = float64(ui.duration.Milliseconds()) // Update video info info := ui.player.GetVideoInfo() ui.fpsLabel.SetText(fmt.Sprintf("FPS: %.2f", info.FrameRate)) return nil } // SeekToTime seeks to a specific time func (ui *FynePlayerUI) SeekToTime(offset time.Duration) error { if err := ui.player.SeekToTime(offset); err != nil { return fmt.Errorf("failed to seek: %w", err) } return nil } // SeekToFrame seeks to a specific frame number func (ui *FynePlayerUI) SeekToFrame(frame int64) error { if err := ui.player.SeekToFrame(frame); err != nil { return fmt.Errorf("failed to seek to frame: %w", err) } return nil } // GetCurrentTime returns the current playback time func (ui *FynePlayerUI) GetCurrentTime() time.Duration { return ui.player.GetCurrentTime() } // GetCurrentFrame returns the current frame number func (ui *FynePlayerUI) GetCurrentFrame() int64 { return ui.player.GetCurrentFrame() } // ExtractFrame extracts a frame at the specified time func (ui *FynePlayerUI) ExtractFrame(offset time.Duration) (interface{}, error) { return ui.player.ExtractFrame(offset) } // EnablePreviewMode enables or disables preview mode func (ui *FynePlayerUI) EnablePreviewMode(enabled bool) { ui.player.EnablePreviewMode(enabled) } // IsPreviewMode returns whether preview mode is enabled func (ui *FynePlayerUI) IsPreviewMode() bool { return ui.player.IsPreviewMode() }