Compare commits

...

9 Commits

Author SHA1 Message Date
62dd39347a feat(ui): Add dialog for installing missing dependencies
When users click on modules with missing dependencies (orange tiles),
show a dialog listing the missing dependencies and their install
commands. This helps users quickly identify what they need to install
to enable the module.
2025-12-31 13:25:59 -05:00
d7175ed04d feat(ui): Add orange background for modules missing dependencies
Modules with handlers but missing dependencies now show orange
background with stripes instead of grey. This distinguishes them
from unimplemented modules (grey) and helps users identify what
needs to be installed.
2025-12-31 13:24:12 -05:00
5fe3c853f4 feat(ui): Mark unimplemented modules as disabled
Set Trim, Audio, and Blu-Ray module handlers to nil to mark them
as disabled. These modules show as grey tiles with lock icons and
diagonal stripes until they are implemented.

Settings remains enabled despite nil handler as it has functionality.
2025-12-31 13:16:10 -05:00
ec51114372 fix(app): Use PNG icon instead of ICO for cross-platform compatibility
Changed icon from .ico to .png format to fix Fyne icon loading
error on Linux. PNG is the proper cross-platform format.
2025-12-31 13:07:56 -05:00
c4d31b31bc fix(ui): Use grey background for disabled modules with white text
Disabled modules now show grey background instead of dimmed colors.
All modules (enabled and disabled) use consistent white text.
2025-12-31 13:06:39 -05:00
9f55604d69 fix(ui): Darken bright module colors for white text readability
Changed module colors to work better with white text:
- Trim: #FFEB3B → #F9A825 (dark yellow/gold)
- Audio: #FFC107 → #FF8F00 (dark amber)
- Subtitles: #8BC34A → #689F38 (dark green)

All modules now use consistent white text for uniform appearance.
2025-12-31 12:59:40 -05:00
7954524bac fix(ui): Prevent crash from nil raster image for enabled modules
The diagonal stripe pattern raster function was returning nil for
enabled modules, causing a nil pointer dereference when Fyne tried
to process the texture. Fixed by always returning a valid image -
transparent for enabled modules, striped for disabled modules.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 12:50:34 -05:00
776ec1f672 feat(ui): Add diagonal stripe pattern to disabled modules
Replaced lock icon-only approach with diagonal stripe overlay for better visual distinction:
- Added static (non-animated) diagonal stripe pattern to disabled tiles
- Stripes use semi-transparent dark overlay (similar to queue progress bars)
- Thicker diagonal lines (8px spacing instead of 4px)
- Pattern clearly distinguishes disabled from enabled modules
- Kept lock icon as secondary indicator

This addresses the issue where adaptive text colors made it difficult to distinguish available vs. disabled modules. The stripe pattern provides immediate visual feedback without relying solely on color dimming.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 12:42:18 -05:00
89824f7859 feat(ui): Add lock icon to disabled modules for better visibility
Enhanced disabled module visual indicators:
- Added lock icon (🔒) in top-right corner of disabled tiles
- Lock icon shows/hides dynamically based on module availability
- Improved Refresh() to handle dynamic enable/disable state changes
- Updated renderer to include lock icon in layout and objects list

This makes it immediately clear which modules are available and which require missing dependencies, addressing the issue where adaptive text colors made disabled modules less distinguishable.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 12:25:24 -05:00
4 changed files with 168 additions and 46 deletions

View File

@ -1,5 +1,5 @@
[Details]
Icon = "assets/logo/VT_Icon.ico"
Icon = "assets/logo/VT_Icon.png"
Name = "VideoTools"
ID = "com.leaktechnologies.videotools"
Version = "0.1.0-dev20"

View File

@ -2,6 +2,7 @@ package ui
import (
"fmt"
"image"
"image/color"
"strings"
"time"
@ -64,20 +65,22 @@ func (m *MonoTheme) Size(name fyne.ThemeSizeName) float32 {
// ModuleTile is a clickable tile widget for module selection
type ModuleTile struct {
widget.BaseWidget
label string
color color.Color
enabled bool
onTapped func()
onDropped func([]fyne.URI)
flashing bool
draggedOver bool
label string
color color.Color
enabled bool
missingDependencies bool
onTapped func()
onDropped func([]fyne.URI)
flashing bool
draggedOver bool
}
// NewModuleTile creates a new module tile
func NewModuleTile(label string, col color.Color, enabled bool, tapped func(), dropped func([]fyne.URI)) *ModuleTile {
func NewModuleTile(label string, col color.Color, enabled bool, missingDeps bool, tapped func(), dropped func([]fyne.URI)) *ModuleTile {
m := &ModuleTile{
label: strings.ToUpper(label),
color: col,
label: strings.ToUpper(label),
color: col,
missingDependencies: missingDeps,
enabled: enabled,
onTapped: tapped,
onDropped: dropped,
@ -147,15 +150,14 @@ func getContrastColor(bgColor color.Color) color.Color {
func (m *ModuleTile) CreateRenderer() fyne.WidgetRenderer {
tileColor := m.color
labelColor := getContrastColor(m.color)
labelColor := TextColor // White text for all modules
// Dim disabled tiles
if !m.enabled {
// Reduce opacity by mixing with dark background
if c, ok := m.color.(color.NRGBA); ok {
tileColor = color.NRGBA{R: c.R / 3, G: c.G / 3, B: c.B / 3, A: c.A}
}
labelColor = color.NRGBA{R: 100, G: 100, B: 100, A: 255}
// Orange background for modules missing dependencies
if m.missingDependencies {
tileColor = color.NRGBA{R: 255, G: 152, B: 0, A: 255} // Orange
} else if !m.enabled {
// Grey background for not implemented modules
tileColor = color.NRGBA{R: 80, G: 80, B: 80, A: 255}
}
bg := canvas.NewRectangle(tileColor)
@ -168,10 +170,45 @@ func (m *ModuleTile) CreateRenderer() fyne.WidgetRenderer {
txt.Alignment = fyne.TextAlignCenter
txt.TextSize = 20
// Lock icon for disabled modules
lockIcon := canvas.NewText("🔒", color.NRGBA{R: 200, G: 200, B: 200, A: 255})
lockIcon.TextSize = 16
lockIcon.Alignment = fyne.TextAlignCenter
if m.enabled {
lockIcon.Hide()
}
// Diagonal stripe overlay for disabled modules
disabledStripe := canvas.NewRaster(func(w, h int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, w, h))
// Only draw stripes if disabled
if !m.enabled {
// Semi-transparent dark stripes
darkStripe := color.NRGBA{R: 0, G: 0, B: 0, A: 100}
lightStripe := color.NRGBA{R: 0, G: 0, B: 0, A: 30}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
// Thicker diagonal stripes (dividing by 8 instead of 4)
if ((x + y) / 8 % 2) == 0 {
img.Set(x, y, darkStripe)
} else {
img.Set(x, y, lightStripe)
}
}
}
}
// Return transparent image for enabled modules
return img
})
return &moduleTileRenderer{
tile: m,
bg: bg,
label: txt,
tile: m,
bg: bg,
label: txt,
lockIcon: lockIcon,
disabledStripe: disabledStripe,
}
}
@ -182,19 +219,38 @@ func (m *ModuleTile) Tapped(*fyne.PointEvent) {
}
type moduleTileRenderer struct {
tile *ModuleTile
bg *canvas.Rectangle
label *canvas.Text
tile *ModuleTile
bg *canvas.Rectangle
label *canvas.Text
lockIcon *canvas.Text
disabledStripe *canvas.Raster
}
func (r *moduleTileRenderer) Layout(size fyne.Size) {
r.bg.Resize(size)
r.bg.Move(fyne.NewPos(0, 0))
// Stripe overlay covers entire tile
if r.disabledStripe != nil {
r.disabledStripe.Resize(size)
r.disabledStripe.Move(fyne.NewPos(0, 0))
}
// Center the label by positioning it in the middle
labelSize := r.label.MinSize()
r.label.Resize(labelSize)
x := (size.Width - labelSize.Width) / 2
y := (size.Height - labelSize.Height) / 2
r.label.Move(fyne.NewPos(x, y))
// Position lock icon in top-right corner
if r.lockIcon != nil {
lockSize := r.lockIcon.MinSize()
r.lockIcon.Resize(lockSize)
lockX := size.Width - lockSize.Width - 4
lockY := float32(4)
r.lockIcon.Move(fyne.NewPos(lockX, lockY))
}
}
func (r *moduleTileRenderer) MinSize() fyne.Size {
@ -202,7 +258,23 @@ func (r *moduleTileRenderer) MinSize() fyne.Size {
}
func (r *moduleTileRenderer) Refresh() {
r.bg.FillColor = r.tile.color
// Update tile color and text color based on enabled state
if r.tile.enabled {
r.bg.FillColor = r.tile.color
r.label.Color = getContrastColor(r.tile.color)
if r.lockIcon != nil {
r.lockIcon.Hide()
}
} else {
// Dim disabled tiles
if c, ok := r.tile.color.(color.NRGBA); ok {
r.bg.FillColor = color.NRGBA{R: c.R / 3, G: c.G / 3, B: c.B / 3, A: c.A}
}
r.label.Color = color.NRGBA{R: 100, G: 100, B: 100, A: 255}
if r.lockIcon != nil {
r.lockIcon.Show()
}
}
// Apply visual feedback based on state
if r.tile.flashing {
@ -222,12 +294,18 @@ func (r *moduleTileRenderer) Refresh() {
r.bg.Refresh()
r.label.Text = r.tile.label
r.label.Refresh()
if r.lockIcon != nil {
r.lockIcon.Refresh()
}
if r.disabledStripe != nil {
r.disabledStripe.Refresh()
}
}
func (r *moduleTileRenderer) Destroy() {}
func (r *moduleTileRenderer) Objects() []fyne.CanvasObject {
return []fyne.CanvasObject{r.bg, r.label}
return []fyne.CanvasObject{r.bg, r.disabledStripe, r.label, r.lockIcon}
}
// TintedBar creates a colored bar container

View File

@ -18,11 +18,12 @@ import (
// ModuleInfo contains information about a module for display
type ModuleInfo struct {
ID string
Label string
Color color.Color
Enabled bool
Category string
ID string
Label string
Color color.Color
Enabled bool
Category string
MissingDependencies bool // true if disabled due to missing dependencies
}
// HistoryEntry represents a completed job in the history
@ -168,8 +169,8 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
// buildModuleTile creates a single module tile
func buildModuleTile(mod ModuleInfo, tapped func(), dropped func([]fyne.URI)) fyne.CanvasObject {
logging.Debug(logging.CatUI, "building tile %s color=%v enabled=%v", mod.ID, mod.Color, mod.Enabled)
return NewModuleTile(mod.Label, mod.Color, mod.Enabled, tapped, dropped)
logging.Debug(logging.CatUI, "building tile %s color=%v enabled=%v missingDeps=%v", mod.ID, mod.Color, mod.Enabled, mod.MissingDependencies)
return NewModuleTile(mod.Label, mod.Color, mod.Enabled, mod.MissingDependencies, tapped, dropped)
}
// buildQueueTile creates the queue status tile

67
main.go
View File

@ -80,18 +80,18 @@ var (
nvencRuntimeOK bool
// Rainbow color palette: balanced ROYGBIV distribution (2 modules per color)
// Bright, vibrant, highly navigable with perfect spectrum balance
// Optimized for white text readability
modulesList = []Module{
{"convert", "Convert", utils.MustHex("#673AB7"), "Convert", modules.HandleConvert}, // Deep Purple (primary conversion)
{"merge", "Merge", utils.MustHex("#4CAF50"), "Convert", modules.HandleMerge}, // Green (combining)
{"trim", "Trim", utils.MustHex("#FFEB3B"), "Convert", modules.HandleTrim}, // Yellow (precision cut)
{"trim", "Trim", utils.MustHex("#F9A825"), "Convert", nil}, // Dark Yellow/Gold (not implemented yet)
{"filters", "Filters", utils.MustHex("#00BCD4"), "Convert", modules.HandleFilters}, // Cyan (creative filters)
{"upscale", "Upscale", utils.MustHex("#9C27B0"), "Advanced", modules.HandleUpscale}, // Purple (AI/advanced)
{"audio", "Audio", utils.MustHex("#FFC107"), "Convert", modules.HandleAudio}, // Amber (sound waves)
{"audio", "Audio", utils.MustHex("#FF8F00"), "Convert", nil}, // Dark Amber (not implemented yet)
{"author", "Author", utils.MustHex("#FF5722"), "Disc", modules.HandleAuthor}, // Deep Orange (authoring)
{"rip", "Rip", utils.MustHex("#FF9800"), "Disc", modules.HandleRip}, // Orange (extraction)
{"bluray", "Blu-Ray", utils.MustHex("#2196F3"), "Disc", modules.HandleBluRay}, // Blue (Blu-ray brand)
{"subtitles", "Subtitles", utils.MustHex("#8BC34A"), "Convert", modules.HandleSubtitles}, // Light Green (text)
{"bluray", "Blu-Ray", utils.MustHex("#2196F3"), "Disc", nil}, // Blue (not implemented yet)
{"subtitles", "Subtitles", utils.MustHex("#689F38"), "Convert", modules.HandleSubtitles}, // Dark Green (text)
{"thumb", "Thumb", utils.MustHex("#00ACC1"), "Screenshots", modules.HandleThumb}, // Dark Cyan (capture)
{"compare", "Compare", utils.MustHex("#E91E63"), "Inspect", modules.HandleCompare}, // Pink (comparison)
{"inspect", "Inspect", utils.MustHex("#F44336"), "Inspect", modules.HandleInspect}, // Red (analysis)
@ -1617,14 +1617,22 @@ func (s *appState) showMainMenu() {
// Convert Module slice to ui.ModuleInfo slice
var mods []ui.ModuleInfo
for _, m := range modulesList {
// Settings module is always enabled
enabled := m.ID == "settings" || isModuleAvailable(m.ID)
hasHandler := m.Handle != nil
depsAvailable := isModuleAvailable(m.ID)
// Module is enabled if: (1) it's Settings (special case) OR (2) it has a handler AND dependencies are available
enabled := m.ID == "settings" || (hasHandler && depsAvailable)
// Missing dependencies = has handler but dependencies not available
missingDeps := hasHandler && !depsAvailable && m.ID != "settings"
mods = append(mods, ui.ModuleInfo{
ID: m.ID,
Label: m.Label,
Color: m.Color,
Category: m.Category,
Enabled: enabled,
ID: m.ID,
Label: m.Label,
Color: m.Color,
Category: m.Category,
Enabled: enabled,
MissingDependencies: missingDeps,
})
}
@ -2665,10 +2673,45 @@ func (s *appState) showBenchmarkHistory() {
s.setContent(view)
}
func (s *appState) showMissingDependenciesDialog(moduleID string) {
missing, _ := getModuleDependencyStatus(moduleID)
if len(missing) == 0 {
return // No missing dependencies
}
// Build message with missing dependencies and install commands
var message strings.Builder
message.WriteString("This module requires the following dependencies:\n\n")
for _, depName := range missing {
if dep, ok := allDependencies[depName]; ok {
message.WriteString(fmt.Sprintf("• %s\n", dep.Name))
if dep.InstallCmd != "" {
message.WriteString(fmt.Sprintf(" Install: %s\n\n", dep.InstallCmd))
}
}
}
// Create dialog
dialog.ShowInformation(
"Missing Dependencies",
message.String(),
s.window,
)
}
func (s *appState) showModule(id string) {
if id != "queue" {
s.stopQueueAutoRefresh()
}
// Check if module has missing dependencies
if !isModuleAvailable(id) && id != "settings" {
s.showMissingDependenciesDialog(id)
return
}
// Track navigation history
s.pushNavigationHistory(id)