Extract inspect module from main.go

Refactoring:
- Create inspect_module.go (292 lines)
- Move showInspectView() and buildInspectView()
- Reduce main.go from 14,329 to 14,116 lines (-213 lines)
- Reduce main.go from 426KB to 420KB

This is the first step in modularizing main.go to improve:
- Windows build performance (currently 5+ minutes)
- Code maintainability and organization
- Following established pattern from author_module.go and subtitles_module.go

Remaining modules to extract:
- player, compare, thumb, filters, upscale, merge, convert, queue, benchmark

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-23 20:51:42 -05:00
parent 1b1657bc21
commit 960def5730
2 changed files with 292 additions and 271 deletions

292
inspect_module.go Normal file
View File

@ -0,0 +1,292 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/interlace"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
func (s *appState) showInspectView() {
s.stopPreview()
s.lastModule = s.active
s.active = "inspect"
s.setContent(buildInspectView(s))
}
// buildInspectView creates the UI for inspecting a single video with player
func buildInspectView(state *appState) fyne.CanvasObject {
inspectColor := moduleColor("inspect")
// Back button
backBtn := widget.NewButton("< INSPECT", func() {
state.showMainMenu()
})
backBtn.Importance = widget.LowImportance
// Top bar with module color
queueBtn := widget.NewButton("View Queue", func() {
state.showQueue()
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(inspectColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
bottomBar := moduleFooter(inspectColor, layout.NewSpacer(), state.statsBar)
// Instructions
instructions := widget.NewLabel("Load a video to inspect its properties and preview playback. Drag a video here or use the button below.")
instructions.Wrapping = fyne.TextWrapWord
instructions.Alignment = fyne.TextAlignCenter
// Clear button
clearBtn := widget.NewButton("Clear", func() {
state.inspectFile = nil
state.showInspectView()
})
clearBtn.Importance = widget.LowImportance
instructionsRow := container.NewBorder(nil, nil, nil, nil, instructions)
// File label
fileLabel := widget.NewLabel("No file loaded")
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
// Metadata text
metadataText := widget.NewLabel("No file loaded")
metadataText.Wrapping = fyne.TextWrapWord
// Metadata scroll
metadataScroll := container.NewScroll(metadataText)
metadataScroll.SetMinSize(fyne.NewSize(400, 200))
// Helper function to format metadata
formatMetadata := func(src *videoSource) string {
fileSize := "Unknown"
if fi, err := os.Stat(src.Path); err == nil {
fileSize = utils.FormatBytes(fi.Size())
}
metadata := fmt.Sprintf(
"━━━ FILE INFO ━━━\n"+
"Path: %s\n"+
"File Size: %s\n"+
"Format Family: %s\n"+
"\n━━━ VIDEO ━━━\n"+
"Codec: %s\n"+
"Resolution: %dx%d\n"+
"Aspect Ratio: %s\n"+
"Frame Rate: %.2f fps\n"+
"Bitrate: %s\n"+
"Pixel Format: %s\n"+
"Color Space: %s\n"+
"Color Range: %s\n"+
"Field Order: %s\n"+
"GOP Size: %d\n"+
"\n━━━ AUDIO ━━━\n"+
"Codec: %s\n"+
"Bitrate: %s\n"+
"Sample Rate: %d Hz\n"+
"Channels: %d\n"+
"\n━━━ OTHER ━━━\n"+
"Duration: %s\n"+
"SAR (Pixel Aspect): %s\n"+
"Chapters: %v\n"+
"Metadata: %v",
filepath.Base(src.Path),
fileSize,
src.Format,
src.VideoCodec,
src.Width, src.Height,
src.AspectRatioString(),
src.FrameRate,
formatBitrateFull(src.Bitrate),
src.PixelFormat,
src.ColorSpace,
src.ColorRange,
src.FieldOrder,
src.GOPSize,
src.AudioCodec,
formatBitrateFull(src.AudioBitrate),
src.AudioRate,
src.Channels,
src.DurationString(),
src.SampleAspectRatio,
src.HasChapters,
src.HasMetadata,
)
// Add interlacing detection results if available
if state.inspectInterlaceAnalyzing {
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
metadata += "Analyzing... (first 500 frames)"
} else if state.inspectInterlaceResult != nil {
result := state.inspectInterlaceResult
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
metadata += fmt.Sprintf("Status: %s\n", result.Status)
metadata += fmt.Sprintf("Interlaced Frames: %.1f%%\n", result.InterlacedPercent)
metadata += fmt.Sprintf("Field Order: %s\n", result.FieldOrder)
metadata += fmt.Sprintf("Confidence: %s\n", result.Confidence)
metadata += fmt.Sprintf("Recommendation: %s\n", result.Recommendation)
metadata += fmt.Sprintf("\nFrame Counts:\n")
metadata += fmt.Sprintf(" Progressive: %d\n", result.Progressive)
metadata += fmt.Sprintf(" Top Field First: %d\n", result.TFF)
metadata += fmt.Sprintf(" Bottom Field First: %d\n", result.BFF)
metadata += fmt.Sprintf(" Undetermined: %d\n", result.Undetermined)
metadata += fmt.Sprintf(" Total Analyzed: %d", result.TotalFrames)
}
return metadata
}
// Video player container
var videoContainer fyne.CanvasObject = container.NewCenter(widget.NewLabel("No video loaded"))
// Update display function
updateDisplay := func() {
if state.inspectFile != nil {
filename := filepath.Base(state.inspectFile.Path)
// Truncate if too long
if len(filename) > 50 {
ext := filepath.Ext(filename)
nameWithoutExt := strings.TrimSuffix(filename, ext)
if len(ext) > 10 {
filename = filename[:47] + "..."
} else {
availableLen := 47 - len(ext)
if availableLen < 1 {
filename = filename[:47] + "..."
} else {
filename = nameWithoutExt[:availableLen] + "..." + ext
}
}
}
fileLabel.SetText(fmt.Sprintf("File: %s", filename))
metadataText.SetText(formatMetadata(state.inspectFile))
// Build video player
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.inspectFile, nil)
} else {
fileLabel.SetText("No file loaded")
metadataText.SetText("No file loaded")
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
}
}
// Initialize display
updateDisplay()
// Load button
loadBtn := widget.NewButton("Load Video", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
}
path := reader.URI().Path()
reader.Close()
src, err := probeVideo(path)
if err != nil {
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
return
}
state.inspectFile = src
state.inspectInterlaceResult = nil
state.inspectInterlaceAnalyzing = true
state.showInspectView()
logging.Debug(logging.CatModule, "loaded inspect file: %s", path)
// Auto-run interlacing detection in background
go func() {
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := detector.QuickAnalyze(ctx, path)
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
state.inspectInterlaceAnalyzing = false
if err != nil {
logging.Debug(logging.CatSystem, "auto interlacing analysis failed: %v", err)
state.inspectInterlaceResult = nil
} else {
state.inspectInterlaceResult = result
logging.Debug(logging.CatSystem, "auto interlacing analysis complete: %s", result.Status)
}
state.showInspectView() // Refresh to show results
}, false)
}()
}, state.window)
})
// Copy metadata button
copyBtn := widget.NewButton("Copy Metadata", func() {
if state.inspectFile == nil {
return
}
metadata := formatMetadata(state.inspectFile)
state.window.Clipboard().SetContent(metadata)
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
})
copyBtn.Importance = widget.LowImportance
logPath := ""
if state.inspectFile != nil {
base := strings.TrimSuffix(filepath.Base(state.inspectFile.Path), filepath.Ext(state.inspectFile.Path))
p := filepath.Join(getLogsDir(), base+conversionLogSuffix)
if _, err := os.Stat(p); err == nil {
logPath = p
}
}
viewLogBtn := widget.NewButton("View Conversion Log", func() {
if logPath == "" {
dialog.ShowInformation("No Log", "No conversion log found for this file.", state.window)
return
}
state.openLogViewer("Conversion Log", logPath, false)
})
viewLogBtn.Importance = widget.LowImportance
if logPath == "" {
viewLogBtn.Disable()
}
// Action buttons
actionButtons := container.NewHBox(loadBtn, copyBtn, viewLogBtn, clearBtn)
// Main layout: left side is video player, right side is metadata
leftColumn := container.NewBorder(
fileLabel,
nil, nil, nil,
videoContainer,
)
rightColumn := container.NewBorder(
widget.NewLabel("Metadata:"),
nil, nil, nil,
metadataScroll,
)
// Bottom bar with module color
bottomBar = moduleFooter(inspectColor, layout.NewSpacer(), state.statsBar)
// Main content
content := container.NewBorder(
container.NewVBox(instructionsRow, actionButtons, widget.NewSeparator()),
nil, nil, nil,
container.NewGridWithColumns(2, leftColumn, rightColumn),
)
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}

271
main.go
View File

@ -2701,13 +2701,6 @@ func (s *appState) showCompareView() {
s.setContent(buildCompareView(s))
}
func (s *appState) showInspectView() {
s.stopPreview()
s.lastModule = s.active
s.active = "inspect"
s.setContent(buildInspectView(s))
}
func (s *appState) showThumbView() {
s.stopPreview()
s.lastModule = s.active
@ -12568,270 +12561,6 @@ func buildCompareView(state *appState) fyne.CanvasObject {
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}
// buildInspectView creates the UI for inspecting a single video with player
func buildInspectView(state *appState) fyne.CanvasObject {
inspectColor := moduleColor("inspect")
// Back button
backBtn := widget.NewButton("< INSPECT", func() {
state.showMainMenu()
})
backBtn.Importance = widget.LowImportance
// Top bar with module color
queueBtn := widget.NewButton("View Queue", func() {
state.showQueue()
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(inspectColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
bottomBar := moduleFooter(inspectColor, layout.NewSpacer(), state.statsBar)
// Instructions
instructions := widget.NewLabel("Load a video to inspect its properties and preview playback. Drag a video here or use the button below.")
instructions.Wrapping = fyne.TextWrapWord
instructions.Alignment = fyne.TextAlignCenter
// Clear button
clearBtn := widget.NewButton("Clear", func() {
state.inspectFile = nil
state.showInspectView()
})
clearBtn.Importance = widget.LowImportance
instructionsRow := container.NewBorder(nil, nil, nil, nil, instructions)
// File label
fileLabel := widget.NewLabel("No file loaded")
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
// Metadata text
metadataText := widget.NewLabel("No file loaded")
metadataText.Wrapping = fyne.TextWrapWord
// Metadata scroll
metadataScroll := container.NewScroll(metadataText)
metadataScroll.SetMinSize(fyne.NewSize(400, 200))
// Helper function to format metadata
formatMetadata := func(src *videoSource) string {
fileSize := "Unknown"
if fi, err := os.Stat(src.Path); err == nil {
fileSize = utils.FormatBytes(fi.Size())
}
metadata := fmt.Sprintf(
"━━━ FILE INFO ━━━\n"+
"Path: %s\n"+
"File Size: %s\n"+
"Format Family: %s\n"+
"\n━━━ VIDEO ━━━\n"+
"Codec: %s\n"+
"Resolution: %dx%d\n"+
"Aspect Ratio: %s\n"+
"Frame Rate: %.2f fps\n"+
"Bitrate: %s\n"+
"Pixel Format: %s\n"+
"Color Space: %s\n"+
"Color Range: %s\n"+
"Field Order: %s\n"+
"GOP Size: %d\n"+
"\n━━━ AUDIO ━━━\n"+
"Codec: %s\n"+
"Bitrate: %s\n"+
"Sample Rate: %d Hz\n"+
"Channels: %d\n"+
"\n━━━ OTHER ━━━\n"+
"Duration: %s\n"+
"SAR (Pixel Aspect): %s\n"+
"Chapters: %v\n"+
"Metadata: %v",
filepath.Base(src.Path),
fileSize,
src.Format,
src.VideoCodec,
src.Width, src.Height,
src.AspectRatioString(),
src.FrameRate,
formatBitrateFull(src.Bitrate),
src.PixelFormat,
src.ColorSpace,
src.ColorRange,
src.FieldOrder,
src.GOPSize,
src.AudioCodec,
formatBitrateFull(src.AudioBitrate),
src.AudioRate,
src.Channels,
src.DurationString(),
src.SampleAspectRatio,
src.HasChapters,
src.HasMetadata,
)
// Add interlacing detection results if available
if state.inspectInterlaceAnalyzing {
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
metadata += "Analyzing... (first 500 frames)"
} else if state.inspectInterlaceResult != nil {
result := state.inspectInterlaceResult
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
metadata += fmt.Sprintf("Status: %s\n", result.Status)
metadata += fmt.Sprintf("Interlaced Frames: %.1f%%\n", result.InterlacedPercent)
metadata += fmt.Sprintf("Field Order: %s\n", result.FieldOrder)
metadata += fmt.Sprintf("Confidence: %s\n", result.Confidence)
metadata += fmt.Sprintf("Recommendation: %s\n", result.Recommendation)
metadata += fmt.Sprintf("\nFrame Counts:\n")
metadata += fmt.Sprintf(" Progressive: %d\n", result.Progressive)
metadata += fmt.Sprintf(" Top Field First: %d\n", result.TFF)
metadata += fmt.Sprintf(" Bottom Field First: %d\n", result.BFF)
metadata += fmt.Sprintf(" Undetermined: %d\n", result.Undetermined)
metadata += fmt.Sprintf(" Total Analyzed: %d", result.TotalFrames)
}
return metadata
}
// Video player container
var videoContainer fyne.CanvasObject = container.NewCenter(widget.NewLabel("No video loaded"))
// Update display function
updateDisplay := func() {
if state.inspectFile != nil {
filename := filepath.Base(state.inspectFile.Path)
// Truncate if too long
if len(filename) > 50 {
ext := filepath.Ext(filename)
nameWithoutExt := strings.TrimSuffix(filename, ext)
if len(ext) > 10 {
filename = filename[:47] + "..."
} else {
availableLen := 47 - len(ext)
if availableLen < 1 {
filename = filename[:47] + "..."
} else {
filename = nameWithoutExt[:availableLen] + "..." + ext
}
}
}
fileLabel.SetText(fmt.Sprintf("File: %s", filename))
metadataText.SetText(formatMetadata(state.inspectFile))
// Build video player
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.inspectFile, nil)
} else {
fileLabel.SetText("No file loaded")
metadataText.SetText("No file loaded")
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
}
}
// Initialize display
updateDisplay()
// Load button
loadBtn := widget.NewButton("Load Video", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
}
path := reader.URI().Path()
reader.Close()
src, err := probeVideo(path)
if err != nil {
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
return
}
state.inspectFile = src
state.inspectInterlaceResult = nil
state.inspectInterlaceAnalyzing = true
state.showInspectView()
logging.Debug(logging.CatModule, "loaded inspect file: %s", path)
// Auto-run interlacing detection in background
go func() {
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := detector.QuickAnalyze(ctx, path)
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
state.inspectInterlaceAnalyzing = false
if err != nil {
logging.Debug(logging.CatSystem, "auto interlacing analysis failed: %v", err)
state.inspectInterlaceResult = nil
} else {
state.inspectInterlaceResult = result
logging.Debug(logging.CatSystem, "auto interlacing analysis complete: %s", result.Status)
}
state.showInspectView() // Refresh to show results
}, false)
}()
}, state.window)
})
// Copy metadata button
copyBtn := widget.NewButton("Copy Metadata", func() {
if state.inspectFile == nil {
return
}
metadata := formatMetadata(state.inspectFile)
state.window.Clipboard().SetContent(metadata)
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
})
copyBtn.Importance = widget.LowImportance
logPath := ""
if state.inspectFile != nil {
base := strings.TrimSuffix(filepath.Base(state.inspectFile.Path), filepath.Ext(state.inspectFile.Path))
p := filepath.Join(getLogsDir(), base+conversionLogSuffix)
if _, err := os.Stat(p); err == nil {
logPath = p
}
}
viewLogBtn := widget.NewButton("View Conversion Log", func() {
if logPath == "" {
dialog.ShowInformation("No Log", "No conversion log found for this file.", state.window)
return
}
state.openLogViewer("Conversion Log", logPath, false)
})
viewLogBtn.Importance = widget.LowImportance
if logPath == "" {
viewLogBtn.Disable()
}
// Action buttons
actionButtons := container.NewHBox(loadBtn, copyBtn, viewLogBtn, clearBtn)
// Main layout: left side is video player, right side is metadata
leftColumn := container.NewBorder(
fileLabel,
nil, nil, nil,
videoContainer,
)
rightColumn := container.NewBorder(
widget.NewLabel("Metadata:"),
nil, nil, nil,
metadataScroll,
)
// Bottom bar with module color
bottomBar = moduleFooter(inspectColor, layout.NewSpacer(), state.statsBar)
// Main content
content := container.NewBorder(
container.NewVBox(instructionsRow, actionButtons, widget.NewSeparator()),
nil, nil, nil,
container.NewGridWithColumns(2, leftColumn, rightColumn),
)
return container.NewBorder(topBar, bottomBar, nil, nil, content)
}
// buildThumbView creates the thumbnail generation UI
func buildThumbView(state *appState) fyne.CanvasObject {