Rename Thumb module to Thumbnail throughout codebase
- Rename thumb_module.go to thumbnail_module.go and thumb_config.go to thumbnail_config.go - Update all function names from showThumbView/addThumbSource/etc to showThumbnailView/addThumbnailSource/etc - Update all struct fields from thumbFile/thumbFiles/etc to thumbnailFile/thumbnailFiles/etc - Update config struct from thumbConfig to thumbnailConfig - Update job type from JobTypeThumb to JobTypeThumbnail - Update module registration from 'thumb' to 'thumbnail' in main.go - Update UI components to show 'THUMBNAIL' instead of 'THUMB' - Update handler function from HandleThumb to HandleThumbnail - Update file naming patterns from 'thumb_*.jpg' to 'thumbnail_*.jpg' - Update author module references to use Thumbnail naming - Update documentation in QUEUE_SYSTEM_GUIDE.md - Maintains all existing functionality while using clearer naming
This commit is contained in:
parent
965242a767
commit
c039ed2753
303
.opencode/plans/videotools-localization-policy.md
Normal file
303
.opencode/plans/videotools-localization-policy.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# VideoTools Localization Policy
|
||||
|
||||
## Overview
|
||||
|
||||
VideoTools uses a single authoritative localization system for all user-facing strings. Fyne's built-in localization is used only for system-level UI elements where we have limited influence. This ensures terminology control, Canadian language priorities, and Indigenous language quality requirements.
|
||||
|
||||
## Language Support Strategy
|
||||
|
||||
### Priority Order
|
||||
|
||||
1. **English (en-CA)** - Source language and authoritative default
|
||||
2. **French (fr-CA)** - Official language parity for Canada
|
||||
3. **Indigenous Languages** - Inuktitut, Cree (phased rollout with human review)
|
||||
4. **Global Languages** - Future expansion when resources allow
|
||||
|
||||
### Language Pack Structure
|
||||
|
||||
```
|
||||
localization/
|
||||
├── en-CA/
|
||||
│ ├── meta.json
|
||||
│ ├── common.json
|
||||
│ ├── convert.json
|
||||
│ ├── merge.json
|
||||
│ ├── player.json
|
||||
│ ├── ui.json
|
||||
│ └── errors.json
|
||||
├── fr-CA/
|
||||
│ ├── meta.json
|
||||
│ ├── [same module structure as en-CA]
|
||||
├── iu/
|
||||
│ ├── meta.json
|
||||
│ ├── common.syllabics.json
|
||||
│ ├── common.roman.json
|
||||
│ ├── convert.syllabics.json
|
||||
│ ├── convert.roman.json
|
||||
│ └── [other modules with dual scripts]
|
||||
├── cr/
|
||||
│ ├── meta.json
|
||||
│ ├── [dual-script structure like iu]
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Authoritative Language Rules
|
||||
|
||||
### Canadian English (en-CA) Policy
|
||||
|
||||
- en-CA is **source and authoritative** language
|
||||
- All localization keys originate in en-CA
|
||||
- Spelling follows Canadian conventions: colour, centre, licence, defence
|
||||
- Technical vocabulary uses North American terms: program (not programme), truck (not lorry)
|
||||
- en-US is a **derived localization**, not a fallback
|
||||
|
||||
### Branding Rules
|
||||
|
||||
#### VT Brand Mark
|
||||
- **VT is never translated**
|
||||
- VT is never rendered in non-Latin scripts
|
||||
- VT is never localized or modified
|
||||
|
||||
#### VideoTools Product Name
|
||||
- Remains untranslated in Latin-script languages
|
||||
- Translated by **meaning only** for non-Latin scripts
|
||||
- Treated as display text, not part of logo asset
|
||||
- Translation must reflect "tools for working with video," not phonetic approximation
|
||||
|
||||
### Script Handling
|
||||
|
||||
#### Indigenous Languages with Dual Scripts
|
||||
- **Primary script**: Syllabics (first-class display)
|
||||
- **Secondary script**: Romanized (accessibility/toggle option)
|
||||
- Both scripts must be **human-reviewed** and approved
|
||||
- No machine translation or automatic transliteration
|
||||
|
||||
#### Script Selection Logic
|
||||
1. User-selected language + primary script
|
||||
2. User-selected language + secondary script (if enabled)
|
||||
3. English (en-CA)
|
||||
4. Safe fallback string
|
||||
|
||||
## Terminology Rules
|
||||
|
||||
### Core Media Concepts
|
||||
|
||||
#### Movie vs Film (Strict Enforcement)
|
||||
- **Movie**: Completed audiovisual work (digital files: MP4, MKV, MOV, AVI)
|
||||
- **Film**: Physical or photochemical medium (8mm, 16mm, 35mm, reels, negatives)
|
||||
- **Never interchangeable** in VT UI or documentation
|
||||
|
||||
#### Technical Terms
|
||||
- **Video**: Visual component of audiovisual signal
|
||||
- **Audio**: Sound component of media file
|
||||
- **Container**: File format encapsulating streams (MP4, MKV, MOV)
|
||||
- **Codec**: Encoding/decoding method (H.264, HEVC, AV1, AAC)
|
||||
- **Stream**: Individual encoded channel within container
|
||||
|
||||
### User Interface Terminology
|
||||
|
||||
#### Standardized Terms
|
||||
- **Tool**: Functional component performing specific task
|
||||
- **Settings**: User-configurable options (preferred over "Preferences")
|
||||
- **Menu**: Navigational UI element grouping actions
|
||||
- **Module**: Major functional area within VT
|
||||
|
||||
#### Avoided Terms
|
||||
Do not use unless explicitly defined:
|
||||
- Media (too broad without context)
|
||||
- Content (unless clearly scoped)
|
||||
- Clip (ambiguous)
|
||||
- Footage (unless raw source material)
|
||||
|
||||
## String Key Architecture
|
||||
|
||||
### Key Naming Convention
|
||||
|
||||
```
|
||||
[module].[category].[item]
|
||||
```
|
||||
|
||||
#### Examples
|
||||
- `convert.output.format` - Output format dropdown label
|
||||
- `merge.chapters.enable` - Chapter preservation checkbox
|
||||
- `player.controls.play` - Play button label
|
||||
- `ui.menu.file` - File menu header
|
||||
- `error.file.not_found` - File not found error message
|
||||
- `common.button.ok` - Generic OK button
|
||||
|
||||
### Key Rules
|
||||
|
||||
- **Semantic keys only** - never English strings as keys
|
||||
- **Hierarchical organization** by module and category
|
||||
- **Stable keys** - once defined, keys do not change
|
||||
- **Descriptive, not terse** - `error.file.not_found` vs `file_error`
|
||||
|
||||
## Localization Implementation
|
||||
|
||||
### Wrapper API Design
|
||||
|
||||
#### Primary Localization Function
|
||||
```go
|
||||
func T(key string, args ...interface{}) string
|
||||
```
|
||||
|
||||
#### Optional Generic Wrapper
|
||||
```go
|
||||
func Generic(key string) string // Only for whitelisted common terms
|
||||
```
|
||||
|
||||
#### Development Guidelines
|
||||
- **Always use T()** for VT-specific content
|
||||
- **Use Generic()** only for approved common terms (OK, Cancel)
|
||||
- **Never mix systems** within single UI component
|
||||
- **All domain/technical terms** use T()
|
||||
|
||||
### Fyne Integration Limits
|
||||
|
||||
#### Fyne Localization Scope
|
||||
- File dialogs (open, save, folder selection)
|
||||
- System error dialogs (where we have no control)
|
||||
- Optional: whitelisted generic terms only
|
||||
|
||||
#### Fyne Translation Treatment
|
||||
- Treated as **non-authoritative** conveniences
|
||||
- Do not let Fyne keys leak into VT string IDs
|
||||
- Never rely on Fyne for VT-specific terminology
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
### Translation Requirements
|
||||
|
||||
#### All Languages
|
||||
- Human translation required
|
||||
- Context provided for all keys
|
||||
- Terminology consistency enforced via glossary
|
||||
- UI testing with actual translations
|
||||
|
||||
#### Indigenous Languages (Additional Requirements)
|
||||
- **Human-reviewed by fluent speaker or language authority**
|
||||
- **No machine translation**
|
||||
- **Syllabics validation** for proper rendering
|
||||
- **Cultural appropriateness review**
|
||||
- **Community feedback** incorporated before release
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
#### Automated Testing
|
||||
- **Translation completeness**: All keys exist in each language
|
||||
- **Key consistency**: No orphaned or deprecated keys
|
||||
- **UI layout expansion**: Test with long strings (pseudo-language)
|
||||
- **Script rendering**: Test syllabics and right-to-left support
|
||||
|
||||
#### Manual Testing
|
||||
- **In-context verification**: All UI elements with actual translations
|
||||
- **Module-by-module validation**: Each module tested independently
|
||||
- **Language switching**: Dynamic switching functionality
|
||||
- **Error conditions**: Error messages with all supported languages
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
### Translation Contributions
|
||||
|
||||
#### General Languages
|
||||
- Pull requests for new translations welcome
|
||||
- Must follow key naming conventions
|
||||
- Include context for ambiguous terms
|
||||
- Maintain consistency with existing terminology
|
||||
|
||||
#### Indigenous Languages
|
||||
- **Must include reviewer credit/approval**
|
||||
- **Dual-script preferred** (syllabics + romanized)
|
||||
- **Cultural notes** included for context
|
||||
- **Community consultation** evidence encouraged
|
||||
|
||||
### Translation Review Process
|
||||
|
||||
#### Reviewer Requirements
|
||||
- Fluent in target language
|
||||
- Familiar with video processing terminology
|
||||
- Understanding of Canadian context (for French/Indigenous languages)
|
||||
|
||||
#### Review Checklist
|
||||
- [ ] Terminology consistency with glossary
|
||||
- [ ] Cultural appropriateness
|
||||
- [ ] Technical accuracy
|
||||
- [ ] Script rendering correctness
|
||||
- [ ] Context preservation
|
||||
|
||||
## Configuration Integration
|
||||
|
||||
### Language Preference Storage
|
||||
|
||||
#### Configuration File
|
||||
```json
|
||||
{
|
||||
"language": "en-CA",
|
||||
"secondaryScript": false,
|
||||
"autoDetect": true,
|
||||
"fallbackLanguage": "en-CA"
|
||||
}
|
||||
```
|
||||
|
||||
#### Fallback Chain
|
||||
1. User-selected language
|
||||
2. User-selected language + secondary script (if enabled)
|
||||
3. English (en-CA)
|
||||
4. Hardcoded emergency string
|
||||
|
||||
### Migration Path
|
||||
|
||||
#### Existing Configurations
|
||||
- Language preference added to existing config files
|
||||
- Backward compatibility maintained
|
||||
- Graceful fallback to en-CA if language not set
|
||||
|
||||
## File Management
|
||||
|
||||
### Translation File Format
|
||||
|
||||
#### JSON Structure
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"language": "Inuktitut",
|
||||
"region": "CA",
|
||||
"script": "syllabics",
|
||||
"reviewer": "Inuit Language Authority",
|
||||
"version": "2024.01"
|
||||
},
|
||||
"translations": {
|
||||
"convert.output.format": "ᐃᓚᓴᐃᔭᕐᓂᖅ ᐱᓕᕆᔭᐅᔪᖅ",
|
||||
"common.button.ok": "ᐱᕙᕚᐅᖅᑐᖅ",
|
||||
"error.file.not_found": "ᐊᓂᑦ ᑎᑎᖃᕐᕕᒃ ᐱᓕᕆᐊᖅᑐᐃᓐᓇᐃᒃ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Validation Rules
|
||||
- Valid JSON structure
|
||||
- All required keys present
|
||||
- No duplicate keys
|
||||
- Proper UTF-8 encoding
|
||||
- Script-appropriate characters
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Scalability
|
||||
- Modular structure allows new language addition without core changes
|
||||
- Separate files per module enable distributed translation work
|
||||
- Metadata system supports language evolution
|
||||
|
||||
### Technical Debt Prevention
|
||||
- Single authoritative system prevents fragmentation
|
||||
- Semantic keys prevent coupling to specific languages
|
||||
- Documentation ensures long-term consistency
|
||||
|
||||
### Community Growth
|
||||
- Clear contribution guidelines enable community participation
|
||||
- Review process maintains quality
|
||||
- Indigenous language requirements ensure cultural respect
|
||||
|
||||
## Status
|
||||
|
||||
This policy is authoritative and binding for all VideoTools localization work. It applies to UI text, documentation, and all external-facing content.
|
||||
276
author_module.go
276
author_module.go
|
|
@ -34,58 +34,58 @@ import (
|
|||
)
|
||||
|
||||
type authorConfig struct {
|
||||
OutputType string `json:"outputType"`
|
||||
Region string `json:"region"`
|
||||
AspectRatio string `json:"aspectRatio"`
|
||||
DiscSize string `json:"discSize"`
|
||||
Title string `json:"title"`
|
||||
CreateMenu bool `json:"createMenu"`
|
||||
MenuTemplate string `json:"menuTemplate"`
|
||||
MenuTheme string `json:"menuTheme"`
|
||||
MenuBackgroundImage string `json:"menuBackgroundImage"`
|
||||
MenuTitleLogoEnabled bool `json:"menuTitleLogoEnabled"`
|
||||
MenuTitleLogoPath string `json:"menuTitleLogoPath"`
|
||||
MenuTitleLogoPosition string `json:"menuTitleLogoPosition"`
|
||||
MenuTitleLogoScale float64 `json:"menuTitleLogoScale"`
|
||||
MenuTitleLogoMargin int `json:"menuTitleLogoMargin"`
|
||||
MenuStudioLogoEnabled bool `json:"menuStudioLogoEnabled"`
|
||||
MenuStudioLogoPath string `json:"menuStudioLogoPath"`
|
||||
MenuStudioLogoPosition string `json:"menuStudioLogoPosition"`
|
||||
MenuStudioLogoScale float64 `json:"menuStudioLogoScale"`
|
||||
MenuStudioLogoMargin int `json:"menuStudioLogoMargin"`
|
||||
MenuStructure string `json:"menuStructure"`
|
||||
MenuExtrasEnabled bool `json:"menuExtrasEnabled"`
|
||||
MenuChapterThumbSrc string `json:"menuChapterThumbSrc"`
|
||||
TreatAsChapters bool `json:"treatAsChapters"`
|
||||
SceneThreshold float64 `json:"sceneThreshold"`
|
||||
OutputType string `json:"outputType"`
|
||||
Region string `json:"region"`
|
||||
AspectRatio string `json:"aspectRatio"`
|
||||
DiscSize string `json:"discSize"`
|
||||
Title string `json:"title"`
|
||||
CreateMenu bool `json:"createMenu"`
|
||||
MenuTemplate string `json:"menuTemplate"`
|
||||
MenuTheme string `json:"menuTheme"`
|
||||
MenuBackgroundImage string `json:"menuBackgroundImage"`
|
||||
MenuTitleLogoEnabled bool `json:"menuTitleLogoEnabled"`
|
||||
MenuTitleLogoPath string `json:"menuTitleLogoPath"`
|
||||
MenuTitleLogoPosition string `json:"menuTitleLogoPosition"`
|
||||
MenuTitleLogoScale float64 `json:"menuTitleLogoScale"`
|
||||
MenuTitleLogoMargin int `json:"menuTitleLogoMargin"`
|
||||
MenuStudioLogoEnabled bool `json:"menuStudioLogoEnabled"`
|
||||
MenuStudioLogoPath string `json:"menuStudioLogoPath"`
|
||||
MenuStudioLogoPosition string `json:"menuStudioLogoPosition"`
|
||||
MenuStudioLogoScale float64 `json:"menuStudioLogoScale"`
|
||||
MenuStudioLogoMargin int `json:"menuStudioLogoMargin"`
|
||||
MenuStructure string `json:"menuStructure"`
|
||||
MenuExtrasEnabled bool `json:"menuExtrasEnabled"`
|
||||
MenuChapterThumbSrc string `json:"menuChapterThumbSrc"`
|
||||
TreatAsChapters bool `json:"treatAsChapters"`
|
||||
SceneThreshold float64 `json:"sceneThreshold"`
|
||||
}
|
||||
|
||||
func defaultAuthorConfig() authorConfig {
|
||||
return authorConfig{
|
||||
OutputType: "dvd",
|
||||
Region: "AUTO",
|
||||
AspectRatio: "AUTO",
|
||||
DiscSize: "DVD5",
|
||||
Title: "",
|
||||
CreateMenu: false,
|
||||
MenuTemplate: "Simple",
|
||||
MenuTheme: "VideoTools",
|
||||
MenuBackgroundImage: "",
|
||||
MenuTitleLogoEnabled: false,
|
||||
MenuTitleLogoPath: "",
|
||||
MenuTitleLogoPosition: "Center",
|
||||
MenuTitleLogoScale: 1.0,
|
||||
MenuTitleLogoMargin: 24,
|
||||
MenuStudioLogoEnabled: true,
|
||||
MenuStudioLogoPath: "",
|
||||
MenuStudioLogoPosition: "Top Right",
|
||||
MenuStudioLogoScale: 1.0,
|
||||
MenuStudioLogoMargin: 24,
|
||||
MenuStructure: "Feature + Chapters",
|
||||
MenuExtrasEnabled: false,
|
||||
MenuChapterThumbSrc: "Auto",
|
||||
TreatAsChapters: false,
|
||||
SceneThreshold: 0.3,
|
||||
OutputType: "dvd",
|
||||
Region: "AUTO",
|
||||
AspectRatio: "AUTO",
|
||||
DiscSize: "DVD5",
|
||||
Title: "",
|
||||
CreateMenu: false,
|
||||
MenuTemplate: "Simple",
|
||||
MenuTheme: "VideoTools",
|
||||
MenuBackgroundImage: "",
|
||||
MenuTitleLogoEnabled: false,
|
||||
MenuTitleLogoPath: "",
|
||||
MenuTitleLogoPosition: "Center",
|
||||
MenuTitleLogoScale: 1.0,
|
||||
MenuTitleLogoMargin: 24,
|
||||
MenuStudioLogoEnabled: true,
|
||||
MenuStudioLogoPath: "",
|
||||
MenuStudioLogoPosition: "Top Right",
|
||||
MenuStudioLogoScale: 1.0,
|
||||
MenuStudioLogoMargin: 24,
|
||||
MenuStructure: "Feature + Chapters",
|
||||
MenuExtrasEnabled: false,
|
||||
MenuChapterThumbSrc: "Auto",
|
||||
TreatAsChapters: false,
|
||||
SceneThreshold: 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,37 +181,37 @@ func (s *appState) applyAuthorConfig(cfg authorConfig) {
|
|||
s.authorMenuStudioLogoMargin = cfg.MenuStudioLogoMargin
|
||||
s.authorMenuStructure = cfg.MenuStructure
|
||||
s.authorMenuExtrasEnabled = cfg.MenuExtrasEnabled
|
||||
s.authorMenuChapterThumbSrc = cfg.MenuChapterThumbSrc
|
||||
s.authorMenuChapterThumbnailSrc = cfg.MenuChapterThumbSrc
|
||||
s.authorTreatAsChapters = cfg.TreatAsChapters
|
||||
s.authorSceneThreshold = cfg.SceneThreshold
|
||||
}
|
||||
|
||||
func (s *appState) persistAuthorConfig() {
|
||||
cfg := authorConfig{
|
||||
OutputType: s.authorOutputType,
|
||||
Region: s.authorRegion,
|
||||
AspectRatio: s.authorAspectRatio,
|
||||
DiscSize: s.authorDiscSize,
|
||||
Title: s.authorTitle,
|
||||
CreateMenu: s.authorCreateMenu,
|
||||
MenuTemplate: s.authorMenuTemplate,
|
||||
MenuTheme: s.authorMenuTheme,
|
||||
MenuBackgroundImage: s.authorMenuBackgroundImage,
|
||||
MenuTitleLogoEnabled: s.authorMenuTitleLogoEnabled,
|
||||
MenuTitleLogoPath: s.authorMenuTitleLogoPath,
|
||||
MenuTitleLogoPosition: s.authorMenuTitleLogoPosition,
|
||||
MenuTitleLogoScale: s.authorMenuTitleLogoScale,
|
||||
MenuTitleLogoMargin: s.authorMenuTitleLogoMargin,
|
||||
MenuStudioLogoEnabled: s.authorMenuStudioLogoEnabled,
|
||||
MenuStudioLogoPath: s.authorMenuStudioLogoPath,
|
||||
MenuStudioLogoPosition: s.authorMenuStudioLogoPosition,
|
||||
MenuStudioLogoScale: s.authorMenuStudioLogoScale,
|
||||
MenuStudioLogoMargin: s.authorMenuStudioLogoMargin,
|
||||
MenuStructure: s.authorMenuStructure,
|
||||
MenuExtrasEnabled: s.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: s.authorMenuChapterThumbSrc,
|
||||
TreatAsChapters: s.authorTreatAsChapters,
|
||||
SceneThreshold: s.authorSceneThreshold,
|
||||
OutputType: s.authorOutputType,
|
||||
Region: s.authorRegion,
|
||||
AspectRatio: s.authorAspectRatio,
|
||||
DiscSize: s.authorDiscSize,
|
||||
Title: s.authorTitle,
|
||||
CreateMenu: s.authorCreateMenu,
|
||||
MenuTemplate: s.authorMenuTemplate,
|
||||
MenuTheme: s.authorMenuTheme,
|
||||
MenuBackgroundImage: s.authorMenuBackgroundImage,
|
||||
MenuTitleLogoEnabled: s.authorMenuTitleLogoEnabled,
|
||||
MenuTitleLogoPath: s.authorMenuTitleLogoPath,
|
||||
MenuTitleLogoPosition: s.authorMenuTitleLogoPosition,
|
||||
MenuTitleLogoScale: s.authorMenuTitleLogoScale,
|
||||
MenuTitleLogoMargin: s.authorMenuTitleLogoMargin,
|
||||
MenuStudioLogoEnabled: s.authorMenuStudioLogoEnabled,
|
||||
MenuStudioLogoPath: s.authorMenuStudioLogoPath,
|
||||
MenuStudioLogoPosition: s.authorMenuStudioLogoPosition,
|
||||
MenuStudioLogoScale: s.authorMenuStudioLogoScale,
|
||||
MenuStudioLogoMargin: s.authorMenuStudioLogoMargin,
|
||||
MenuStructure: s.authorMenuStructure,
|
||||
MenuExtrasEnabled: s.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: s.authorMenuChapterThumbnailSrc,
|
||||
TreatAsChapters: s.authorTreatAsChapters,
|
||||
SceneThreshold: s.authorSceneThreshold,
|
||||
}
|
||||
if err := savePersistedAuthorConfig(cfg); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to persist author config: %v", err)
|
||||
|
|
@ -266,8 +266,8 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
|
|||
if state.authorMenuStructure == "" {
|
||||
state.authorMenuStructure = "Feature + Chapters"
|
||||
}
|
||||
if state.authorMenuChapterThumbSrc == "" {
|
||||
state.authorMenuChapterThumbSrc = "Auto"
|
||||
if state.authorMenuChapterThumbnailSrc == "" {
|
||||
state.authorMenuChapterThumbnailSrc = "Auto"
|
||||
}
|
||||
|
||||
authorColor := moduleColor("author")
|
||||
|
|
@ -904,30 +904,30 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|||
|
||||
saveCfgBtn := widget.NewButton("Save Config", func() {
|
||||
cfg := authorConfig{
|
||||
OutputType: state.authorOutputType,
|
||||
Region: state.authorRegion,
|
||||
AspectRatio: state.authorAspectRatio,
|
||||
DiscSize: state.authorDiscSize,
|
||||
Title: state.authorTitle,
|
||||
CreateMenu: state.authorCreateMenu,
|
||||
MenuTemplate: state.authorMenuTemplate,
|
||||
MenuTheme: state.authorMenuTheme,
|
||||
MenuBackgroundImage: state.authorMenuBackgroundImage,
|
||||
MenuTitleLogoEnabled: state.authorMenuTitleLogoEnabled,
|
||||
MenuTitleLogoPath: state.authorMenuTitleLogoPath,
|
||||
MenuTitleLogoPosition: state.authorMenuTitleLogoPosition,
|
||||
MenuTitleLogoScale: state.authorMenuTitleLogoScale,
|
||||
MenuTitleLogoMargin: state.authorMenuTitleLogoMargin,
|
||||
MenuStudioLogoEnabled: state.authorMenuStudioLogoEnabled,
|
||||
MenuStudioLogoPath: state.authorMenuStudioLogoPath,
|
||||
MenuStudioLogoPosition: state.authorMenuStudioLogoPosition,
|
||||
MenuStudioLogoScale: state.authorMenuStudioLogoScale,
|
||||
MenuStudioLogoMargin: state.authorMenuStudioLogoMargin,
|
||||
MenuStructure: state.authorMenuStructure,
|
||||
MenuExtrasEnabled: state.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: state.authorMenuChapterThumbSrc,
|
||||
TreatAsChapters: state.authorTreatAsChapters,
|
||||
SceneThreshold: state.authorSceneThreshold,
|
||||
OutputType: state.authorOutputType,
|
||||
Region: state.authorRegion,
|
||||
AspectRatio: state.authorAspectRatio,
|
||||
DiscSize: state.authorDiscSize,
|
||||
Title: state.authorTitle,
|
||||
CreateMenu: state.authorCreateMenu,
|
||||
MenuTemplate: state.authorMenuTemplate,
|
||||
MenuTheme: state.authorMenuTheme,
|
||||
MenuBackgroundImage: state.authorMenuBackgroundImage,
|
||||
MenuTitleLogoEnabled: state.authorMenuTitleLogoEnabled,
|
||||
MenuTitleLogoPath: state.authorMenuTitleLogoPath,
|
||||
MenuTitleLogoPosition: state.authorMenuTitleLogoPosition,
|
||||
MenuTitleLogoScale: state.authorMenuTitleLogoScale,
|
||||
MenuTitleLogoMargin: state.authorMenuTitleLogoMargin,
|
||||
MenuStudioLogoEnabled: state.authorMenuStudioLogoEnabled,
|
||||
MenuStudioLogoPath: state.authorMenuStudioLogoPath,
|
||||
MenuStudioLogoPosition: state.authorMenuStudioLogoPosition,
|
||||
MenuStudioLogoScale: state.authorMenuStudioLogoScale,
|
||||
MenuStudioLogoMargin: state.authorMenuStudioLogoMargin,
|
||||
MenuStructure: state.authorMenuStructure,
|
||||
MenuExtrasEnabled: state.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: state.authorMenuChapterThumbnailSrc,
|
||||
TreatAsChapters: state.authorTreatAsChapters,
|
||||
SceneThreshold: state.authorSceneThreshold,
|
||||
}
|
||||
if err := savePersistedAuthorConfig(cfg); err != nil {
|
||||
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
|
||||
|
|
@ -1319,13 +1319,13 @@ func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
|||
"Midpoint",
|
||||
"Custom (Advanced)",
|
||||
}, func(value string) {
|
||||
state.authorMenuChapterThumbSrc = value
|
||||
state.authorMenuChapterThumbnailSrc = value
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
if state.authorMenuChapterThumbSrc == "" {
|
||||
state.authorMenuChapterThumbSrc = "Auto"
|
||||
if state.authorMenuChapterThumbnailSrc == "" {
|
||||
state.authorMenuChapterThumbnailSrc = "Auto"
|
||||
}
|
||||
thumbSourceSelect.SetSelected(state.authorMenuChapterThumbSrc)
|
||||
thumbSourceSelect.SetSelected(state.authorMenuChapterThumbnailSrc)
|
||||
|
||||
info := widget.NewLabel("DVD menus are generated using the VideoTools theme and IBM Plex Mono. Menu settings apply only to disc authoring.")
|
||||
info.Wrapping = fyne.TextWrapWord
|
||||
|
|
@ -2396,40 +2396,40 @@ func (s *appState) addAuthorToQueue(paths []string, region, aspect, title, outpu
|
|||
}
|
||||
|
||||
config := map[string]interface{}{
|
||||
"paths": paths,
|
||||
"region": region,
|
||||
"aspect": aspect,
|
||||
"title": title,
|
||||
"outputPath": outputPath,
|
||||
"makeISO": makeISO,
|
||||
"treatAsChapters": s.authorTreatAsChapters,
|
||||
"clips": clips,
|
||||
"chapters": chapters,
|
||||
"discSize": s.authorDiscSize,
|
||||
"outputType": s.authorOutputType,
|
||||
"authorTitle": s.authorTitle,
|
||||
"authorRegion": s.authorRegion,
|
||||
"authorAspect": s.authorAspectRatio,
|
||||
"createMenu": s.authorCreateMenu,
|
||||
"chapterSource": s.authorChapterSource,
|
||||
"subtitleTracks": append([]string{}, s.authorSubtitles...),
|
||||
"additionalAudios": append([]string{}, s.authorAudioTracks...),
|
||||
"menuTemplate": s.authorMenuTemplate,
|
||||
"menuBackgroundImage": s.authorMenuBackgroundImage,
|
||||
"menuTheme": s.authorMenuTheme,
|
||||
"menuTitleLogoEnabled": s.authorMenuTitleLogoEnabled,
|
||||
"menuTitleLogoPath": s.authorMenuTitleLogoPath,
|
||||
"menuTitleLogoPosition": s.authorMenuTitleLogoPosition,
|
||||
"menuTitleLogoScale": s.authorMenuTitleLogoScale,
|
||||
"menuTitleLogoMargin": s.authorMenuTitleLogoMargin,
|
||||
"menuStudioLogoEnabled": s.authorMenuStudioLogoEnabled,
|
||||
"menuStudioLogoPath": s.authorMenuStudioLogoPath,
|
||||
"menuStudioLogoPosition": s.authorMenuStudioLogoPosition,
|
||||
"menuStudioLogoScale": s.authorMenuStudioLogoScale,
|
||||
"menuStudioLogoMargin": s.authorMenuStudioLogoMargin,
|
||||
"menuStructure": s.authorMenuStructure,
|
||||
"menuExtrasEnabled": s.authorMenuExtrasEnabled,
|
||||
"menuChapterThumbSrc": s.authorMenuChapterThumbSrc,
|
||||
"paths": paths,
|
||||
"region": region,
|
||||
"aspect": aspect,
|
||||
"title": title,
|
||||
"outputPath": outputPath,
|
||||
"makeISO": makeISO,
|
||||
"treatAsChapters": s.authorTreatAsChapters,
|
||||
"clips": clips,
|
||||
"chapters": chapters,
|
||||
"discSize": s.authorDiscSize,
|
||||
"outputType": s.authorOutputType,
|
||||
"authorTitle": s.authorTitle,
|
||||
"authorRegion": s.authorRegion,
|
||||
"authorAspect": s.authorAspectRatio,
|
||||
"createMenu": s.authorCreateMenu,
|
||||
"chapterSource": s.authorChapterSource,
|
||||
"subtitleTracks": append([]string{}, s.authorSubtitles...),
|
||||
"additionalAudios": append([]string{}, s.authorAudioTracks...),
|
||||
"menuTemplate": s.authorMenuTemplate,
|
||||
"menuBackgroundImage": s.authorMenuBackgroundImage,
|
||||
"menuTheme": s.authorMenuTheme,
|
||||
"menuTitleLogoEnabled": s.authorMenuTitleLogoEnabled,
|
||||
"menuTitleLogoPath": s.authorMenuTitleLogoPath,
|
||||
"menuTitleLogoPosition": s.authorMenuTitleLogoPosition,
|
||||
"menuTitleLogoScale": s.authorMenuTitleLogoScale,
|
||||
"menuTitleLogoMargin": s.authorMenuTitleLogoMargin,
|
||||
"menuStudioLogoEnabled": s.authorMenuStudioLogoEnabled,
|
||||
"menuStudioLogoPath": s.authorMenuStudioLogoPath,
|
||||
"menuStudioLogoPosition": s.authorMenuStudioLogoPosition,
|
||||
"menuStudioLogoScale": s.authorMenuStudioLogoScale,
|
||||
"menuStudioLogoMargin": s.authorMenuStudioLogoMargin,
|
||||
"menuStructure": s.authorMenuStructure,
|
||||
"menuExtrasEnabled": s.authorMenuExtrasEnabled,
|
||||
"menuChapterThumbSrc": s.authorMenuChapterThumbnailSrc,
|
||||
}
|
||||
|
||||
titleLabel := title
|
||||
|
|
@ -3725,7 +3725,7 @@ func extractChapterThumbnail(videoPath string, timestamp float64) (string, error
|
|||
return "", err
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(tmpDir, fmt.Sprintf("thumb_%.2f.jpg", timestamp))
|
||||
outputPath := filepath.Join(tmpDir, fmt.Sprintf("thumbnail_%.2f.jpg", timestamp))
|
||||
args := []string{
|
||||
"-ss", fmt.Sprintf("%.2f", timestamp),
|
||||
"-i", videoPath,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const (
|
|||
JobTypeFilter JobType = "filter" // Effects/filters
|
||||
JobTypeUpscale JobType = "upscale" // Video enhancement
|
||||
JobTypeAudio JobType = "audio" // Audio processing
|
||||
JobTypeThumb JobType = "thumb" // Thumbnail generation
|
||||
JobTypeThumbnail JobType = "thumbnail" // Thumbnail generation
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ func HandleSubtitles(files []string) {
|
|||
fmt.Println("subtitles", files)
|
||||
}
|
||||
|
||||
// HandleThumb handles the thumb module
|
||||
func HandleThumb(files []string) {
|
||||
logging.Debug(logging.CatModule, "thumb handler invoked with %v", files)
|
||||
fmt.Println("thumb", files)
|
||||
// HandleThumbnail handles the thumbnail module
|
||||
func HandleThumbnail(files []string) {
|
||||
logging.Debug(logging.CatModule, "thumbnail handler invoked with %v", files)
|
||||
fmt.Println("thumbnail", files)
|
||||
}
|
||||
|
||||
// HandleInspect handles the inspect module
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const (
|
|||
JobTypeRip JobType = "rip"
|
||||
JobTypeBluray JobType = "bluray"
|
||||
JobTypeSubtitles JobType = "subtitles"
|
||||
JobTypeThumb JobType = "thumb"
|
||||
JobTypeThumbnail JobType = "thumbnail"
|
||||
JobTypeInspect JobType = "inspect"
|
||||
JobTypeCompare JobType = "compare"
|
||||
JobTypePlayer JobType = "player"
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ func (g *Generator) generateIndividual(ctx context.Context, config Config, durat
|
|||
|
||||
// Generate each thumbnail
|
||||
for i, ts := range timestamps {
|
||||
outputPath := filepath.Join(config.OutputDir, fmt.Sprintf("thumb_%04d.%s", i+1, config.Format))
|
||||
outputPath := filepath.Join(config.OutputDir, fmt.Sprintf("thumbnail_%04d.%s", i+1, config.Format))
|
||||
|
||||
// Build FFmpeg command
|
||||
args := []string{
|
||||
|
|
|
|||
|
|
@ -1069,9 +1069,9 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject {
|
|||
case queue.JobTypeAudio:
|
||||
badgeColor = utils.MustHex("#FFC107") // Amber
|
||||
badgeText = "AUDIO"
|
||||
case queue.JobTypeThumb:
|
||||
case queue.JobTypeThumbnail:
|
||||
badgeColor = utils.MustHex("#00ACC1") // Dark Cyan
|
||||
badgeText = "THUMB"
|
||||
badgeText = "THUMBNAIL"
|
||||
case queue.JobTypeSnippet:
|
||||
badgeColor = utils.MustHex("#00BCD4") // Cyan (same as Convert)
|
||||
badgeText = "SNIPPET"
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ func ModuleColor(t queue.JobType) color.Color {
|
|||
return color.RGBA{R: 156, G: 39, B: 176, A: 255} // Purple (#9C27B0)
|
||||
case queue.JobTypeAudio:
|
||||
return color.RGBA{R: 255, G: 193, B: 7, A: 255} // Amber (#FFC107)
|
||||
case queue.JobTypeThumb:
|
||||
case queue.JobTypeThumbnail:
|
||||
return color.RGBA{R: 0, G: 172, B: 193, A: 255} // Dark Cyan (#00ACC1)
|
||||
case queue.JobTypeAuthor:
|
||||
return color.RGBA{R: 255, G: 87, B: 34, A: 255} // Deep Orange (#FF5722)
|
||||
|
|
|
|||
170
main.go
170
main.go
|
|
@ -96,23 +96,23 @@ var (
|
|||
// Rainbow color palette: balanced ROYGBIV distribution (2 modules per color)
|
||||
// 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("#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)
|
||||
{"enhancement", "Enhancement", utils.MustHex("#7C3AED"), "Advanced", modules.HandleEnhance}, // Cyan (AI enhancement)
|
||||
{"audio", "Audio", utils.MustHex("#FF8F00"), "Convert", modules.HandleAudio}, // Dark Amber - audio extraction
|
||||
{"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", nil}, // Blue (not implemented yet)
|
||||
{"subtitles", "Subtitles", utils.MustHex("#689F38"), "Convert", modules.HandleSubtitles}, // Dark Green (text)
|
||||
{"enhancement", "Enhancement", utils.MustHex("#7C3AED"), "Advanced", modules.HandleEnhance}, // Cyan (AI enhancement)
|
||||
{"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)
|
||||
{"player", "Player", utils.MustHex("#3F51B5"), "Playback", modules.HandlePlayer}, // Indigo (playback)
|
||||
{"settings", "Settings", utils.MustHex("#607D8B"), "Settings", nil}, // Blue Grey (settings)
|
||||
{"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("#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)
|
||||
{"enhancement", "Enhancement", utils.MustHex("#7C3AED"), "Advanced", modules.HandleEnhance}, // Cyan (AI enhancement)
|
||||
{"audio", "Audio", utils.MustHex("#FF8F00"), "Convert", modules.HandleAudio}, // Dark Amber - audio extraction
|
||||
{"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", nil}, // Blue (not implemented yet)
|
||||
{"subtitles", "Subtitles", utils.MustHex("#689F38"), "Convert", modules.HandleSubtitles}, // Dark Green (text)
|
||||
{"enhancement", "Enhancement", utils.MustHex("#7C3AED"), "Advanced", modules.HandleEnhance}, // Cyan (AI enhancement)
|
||||
{"thumbnail", "Thumbnail", utils.MustHex("#00ACC1"), "Screenshots", modules.HandleThumbnail}, // Dark Cyan (capture)
|
||||
{"compare", "Compare", utils.MustHex("#E91E63"), "Inspect", modules.HandleCompare}, // Pink (comparison)
|
||||
{"inspect", "Inspect", utils.MustHex("#F44336"), "Inspect", modules.HandleInspect}, // Red (analysis)
|
||||
{"player", "Player", utils.MustHex("#3F51B5"), "Playback", modules.HandlePlayer}, // Indigo (playback)
|
||||
{"settings", "Settings", utils.MustHex("#607D8B"), "Settings", nil}, // Blue Grey (settings)
|
||||
}
|
||||
|
||||
// Platform-specific configuration
|
||||
|
|
@ -1075,16 +1075,16 @@ type appState struct {
|
|||
mergeMotionInterpolation bool // Use motion interpolation for frame rate changes
|
||||
|
||||
// Thumbnail module state
|
||||
thumbFile *videoSource
|
||||
thumbFiles []*videoSource
|
||||
thumbCount int
|
||||
thumbWidth int
|
||||
thumbContactSheet bool
|
||||
thumbShowTimestamps bool
|
||||
thumbSheetWidth int
|
||||
thumbColumns int
|
||||
thumbRows int
|
||||
thumbLastOutputPath string // Path to last generated output
|
||||
thumbnailFile *videoSource
|
||||
thumbnailFiles []*videoSource
|
||||
thumbnailCount int
|
||||
thumbnailWidth int
|
||||
thumbnailContactSheet bool
|
||||
thumbnailShowTimestamps bool
|
||||
thumbnailSheetWidth int
|
||||
thumbnailColumns int
|
||||
thumbnailRows int
|
||||
thumbnailLastOutputPath string // Path to last generated output
|
||||
|
||||
// Player module state
|
||||
playerFile *videoSource
|
||||
|
|
@ -1165,49 +1165,49 @@ type appState struct {
|
|||
historyTabIdx int
|
||||
|
||||
// Author module state
|
||||
authorFile *videoSource
|
||||
authorChapters []authorChapter
|
||||
authorSceneThreshold float64
|
||||
authorDetecting bool
|
||||
authorClips []authorClip // Multiple video clips for compilation
|
||||
authorOutputType string // "dvd" or "iso"
|
||||
authorRegion string // "NTSC", "PAL", "AUTO"
|
||||
authorAspectRatio string // "4:3", "16:9", "AUTO"
|
||||
authorCreateMenu bool // Whether to create DVD menu
|
||||
authorMenuTemplate string // "Simple", "Dark", "Poster"
|
||||
authorMenuBackgroundImage string // Path to a user-selected background image
|
||||
authorMenuTheme string // "VideoTools"
|
||||
authorMenuTitleLogoEnabled bool // Enable title logo (main logo above menu)
|
||||
authorMenuTitleLogoPath string // Path to title logo image
|
||||
authorMenuTitleLogoPosition string // Position for title logo
|
||||
authorMenuTitleLogoScale float64 // Scale for title logo
|
||||
authorMenuTitleLogoMargin int // Margin for title logo
|
||||
authorMenuStudioLogoEnabled bool // Enable studio logo (corner logo)
|
||||
authorMenuStudioLogoPath string // Path to studio logo image
|
||||
authorMenuStudioLogoPosition string // "Top Left", "Top Right", "Bottom Left", "Bottom Right"
|
||||
authorMenuStudioLogoScale float64 // Scale for studio logo
|
||||
authorMenuStudioLogoMargin int // Margin for studio logo
|
||||
authorMenuStructure string // Feature only, Chapters, Extras
|
||||
authorMenuExtrasEnabled bool // Show extras menu
|
||||
authorMenuChapterThumbSrc string // Auto, First Frame, Midpoint, Custom
|
||||
authorTitle string // DVD title
|
||||
authorSubtitles []string // Subtitle file paths
|
||||
authorAudioTracks []string // Additional audio tracks
|
||||
authorSummaryLabel *widget.Label
|
||||
authorTreatAsChapters bool // Treat multiple clips as chapters
|
||||
authorChapterSource string // embedded, scenes, clips, manual
|
||||
authorChaptersRefresh func() // Refresh hook for chapter list UI
|
||||
authorDiscSize string // "DVD5" or "DVD9"
|
||||
authorLogText string
|
||||
authorLogLines []string // Circular buffer for last N lines
|
||||
authorLogFilePath string // Path to log file for full viewing
|
||||
authorLogEntry *widget.Entry
|
||||
authorLogScroll *container.Scroll
|
||||
authorProgress float64
|
||||
authorProgressBar *widget.ProgressBar
|
||||
authorStatusLabel *widget.Label
|
||||
authorCancelBtn *widget.Button
|
||||
authorVideoTSPath string
|
||||
authorFile *videoSource
|
||||
authorChapters []authorChapter
|
||||
authorSceneThreshold float64
|
||||
authorDetecting bool
|
||||
authorClips []authorClip // Multiple video clips for compilation
|
||||
authorOutputType string // "dvd" or "iso"
|
||||
authorRegion string // "NTSC", "PAL", "AUTO"
|
||||
authorAspectRatio string // "4:3", "16:9", "AUTO"
|
||||
authorCreateMenu bool // Whether to create DVD menu
|
||||
authorMenuTemplate string // "Simple", "Dark", "Poster"
|
||||
authorMenuBackgroundImage string // Path to a user-selected background image
|
||||
authorMenuTheme string // "VideoTools"
|
||||
authorMenuTitleLogoEnabled bool // Enable title logo (main logo above menu)
|
||||
authorMenuTitleLogoPath string // Path to title logo image
|
||||
authorMenuTitleLogoPosition string // Position for title logo
|
||||
authorMenuTitleLogoScale float64 // Scale for title logo
|
||||
authorMenuTitleLogoMargin int // Margin for title logo
|
||||
authorMenuStudioLogoEnabled bool // Enable studio logo (corner logo)
|
||||
authorMenuStudioLogoPath string // Path to studio logo image
|
||||
authorMenuStudioLogoPosition string // "Top Left", "Top Right", "Bottom Left", "Bottom Right"
|
||||
authorMenuStudioLogoScale float64 // Scale for studio logo
|
||||
authorMenuStudioLogoMargin int // Margin for studio logo
|
||||
authorMenuStructure string // Feature only, Chapters, Extras
|
||||
authorMenuExtrasEnabled bool // Show extras menu
|
||||
authorMenuChapterThumbnailSrc string // Auto, First Frame, Midpoint, Custom
|
||||
authorTitle string // DVD title
|
||||
authorSubtitles []string // Subtitle file paths
|
||||
authorAudioTracks []string // Additional audio tracks
|
||||
authorSummaryLabel *widget.Label
|
||||
authorTreatAsChapters bool // Treat multiple clips as chapters
|
||||
authorChapterSource string // embedded, scenes, clips, manual
|
||||
authorChaptersRefresh func() // Refresh hook for chapter list UI
|
||||
authorDiscSize string // "DVD5" or "DVD9"
|
||||
authorLogText string
|
||||
authorLogLines []string // Circular buffer for last N lines
|
||||
authorLogFilePath string // Path to log file for full viewing
|
||||
authorLogEntry *widget.Entry
|
||||
authorLogScroll *container.Scroll
|
||||
authorProgress float64
|
||||
authorProgressBar *widget.ProgressBar
|
||||
authorStatusLabel *widget.Label
|
||||
authorCancelBtn *widget.Button
|
||||
authorVideoTSPath string
|
||||
|
||||
// Rip module state
|
||||
ripSourcePath string
|
||||
|
|
@ -3101,8 +3101,8 @@ func (s *appState) showModule(id string) {
|
|||
s.showCompareView()
|
||||
case "inspect":
|
||||
s.showInspectView()
|
||||
case "thumb":
|
||||
s.showThumbView()
|
||||
case "thumbnail":
|
||||
s.showThumbnailView()
|
||||
case "player":
|
||||
s.showPlayerView()
|
||||
case "filters":
|
||||
|
|
@ -3310,13 +3310,13 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
|||
return
|
||||
}
|
||||
|
||||
// If thumb module, load video into thumb slot
|
||||
if moduleID == "thumb" {
|
||||
// If thumbnail module, load video into thumbnail slot
|
||||
if moduleID == "thumbnail" {
|
||||
path := videoPaths[0]
|
||||
go func() {
|
||||
src, err := probeVideo(path)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatModule, "failed to load video for thumb: %v", err)
|
||||
logging.Debug(logging.CatModule, "failed to load video for thumbnail: %v", err)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
||||
}, false)
|
||||
|
|
@ -3326,9 +3326,9 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
|||
// Update state and show module (with small delay to allow flash animation)
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.thumbFile = src
|
||||
s.thumbnailFile = src
|
||||
s.showModule(moduleID)
|
||||
logging.Debug(logging.CatModule, "loaded video for thumb module")
|
||||
logging.Debug(logging.CatModule, "loaded video for thumbnail module")
|
||||
}, false)
|
||||
}()
|
||||
return
|
||||
|
|
@ -4192,8 +4192,8 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
|
|||
return s.executeUpscaleJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeAudio:
|
||||
return s.executeAudioJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeThumb:
|
||||
return s.executeThumbJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeThumbnail:
|
||||
return s.executeThumbnailJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeSnippet:
|
||||
return s.executeSnippetJob(ctx, job, progressCallback)
|
||||
case queue.JobTypeAuthor:
|
||||
|
|
@ -6828,8 +6828,8 @@ func runCLI(args []string) error {
|
|||
modules.HandleUpscale(cmdArgs)
|
||||
case "audio":
|
||||
modules.HandleAudio(cmdArgs)
|
||||
case "thumb":
|
||||
modules.HandleThumb(cmdArgs)
|
||||
case "thumbnail":
|
||||
modules.HandleThumbnail(cmdArgs)
|
||||
case "compare":
|
||||
modules.HandleCompare(cmdArgs)
|
||||
case "inspect":
|
||||
|
|
@ -6895,7 +6895,7 @@ func printUsage() {
|
|||
fmt.Println(" videotools filters <args>")
|
||||
fmt.Println(" videotools upscale <args>")
|
||||
fmt.Println(" videotools audio <args>")
|
||||
fmt.Println(" videotools thumb <args>")
|
||||
fmt.Println(" videotools thumbnail <args>")
|
||||
fmt.Println(" videotools compare <file1> <file2>")
|
||||
fmt.Println(" videotools inspect <args>")
|
||||
fmt.Println(" videotools logs # tail recent log lines")
|
||||
|
|
@ -11018,7 +11018,7 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
dlg.Show()
|
||||
})
|
||||
|
||||
usePlayer := state.active != "thumb"
|
||||
usePlayer := state.active != "thumbnail"
|
||||
|
||||
currentTime := widget.NewLabel("0:00")
|
||||
totalTime := widget.NewLabel(src.DurationString())
|
||||
|
|
@ -12483,8 +12483,8 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
return
|
||||
}
|
||||
|
||||
// If in thumb module, handle single video file
|
||||
if s.active == "thumb" {
|
||||
// If in thumbnail module, handle single video file
|
||||
if s.active == "thumbnail" {
|
||||
// Collect video files from dropped items
|
||||
var videoPaths []string
|
||||
for _, uri := range items {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
||||
)
|
||||
|
||||
type thumbConfig struct {
|
||||
type thumbnailConfig struct {
|
||||
ContactSheet bool `json:"contactSheet"`
|
||||
ShowTimestamps bool `json:"showTimestamps"`
|
||||
Count int `json:"count"`
|
||||
|
|
@ -18,8 +18,8 @@ type thumbConfig struct {
|
|||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
func defaultThumbConfig() thumbConfig {
|
||||
return thumbConfig{
|
||||
func defaultThumbnailConfig() thumbnailConfig {
|
||||
return thumbnailConfig{
|
||||
ContactSheet: false,
|
||||
ShowTimestamps: false,
|
||||
Count: 24,
|
||||
|
|
@ -30,9 +30,9 @@ func defaultThumbConfig() thumbConfig {
|
|||
}
|
||||
}
|
||||
|
||||
func loadPersistedThumbConfig() (thumbConfig, error) {
|
||||
var cfg thumbConfig
|
||||
path := moduleConfigPath("thumb")
|
||||
func loadPersistedThumbnailConfig() (thumbnailConfig, error) {
|
||||
var cfg thumbnailConfig
|
||||
path := moduleConfigPath("thumbnail")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
|
|
@ -58,8 +58,8 @@ func loadPersistedThumbConfig() (thumbConfig, error) {
|
|||
return cfg, nil
|
||||
}
|
||||
|
||||
func savePersistedThumbConfig(cfg thumbConfig) error {
|
||||
path := moduleConfigPath("thumb")
|
||||
func savePersistedThumbnailConfig(cfg thumbnailConfig) error {
|
||||
path := moduleConfigPath("thumbnail")
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -70,27 +70,27 @@ func savePersistedThumbConfig(cfg thumbConfig) error {
|
|||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
func (s *appState) applyThumbConfig(cfg thumbConfig) {
|
||||
s.thumbContactSheet = cfg.ContactSheet
|
||||
s.thumbShowTimestamps = cfg.ShowTimestamps
|
||||
s.thumbCount = cfg.Count
|
||||
s.thumbWidth = cfg.Width
|
||||
s.thumbSheetWidth = cfg.SheetWidth
|
||||
s.thumbColumns = cfg.Columns
|
||||
s.thumbRows = cfg.Rows
|
||||
func (s *appState) applyThumbnailConfig(cfg thumbnailConfig) {
|
||||
s.thumbnailContactSheet = cfg.ContactSheet
|
||||
s.thumbnailShowTimestamps = cfg.ShowTimestamps
|
||||
s.thumbnailCount = cfg.Count
|
||||
s.thumbnailWidth = cfg.Width
|
||||
s.thumbnailSheetWidth = cfg.SheetWidth
|
||||
s.thumbnailColumns = cfg.Columns
|
||||
s.thumbnailRows = cfg.Rows
|
||||
}
|
||||
|
||||
func (s *appState) persistThumbConfig() {
|
||||
cfg := thumbConfig{
|
||||
ContactSheet: s.thumbContactSheet,
|
||||
ShowTimestamps: s.thumbShowTimestamps,
|
||||
Count: s.thumbCount,
|
||||
Width: s.thumbWidth,
|
||||
SheetWidth: s.thumbSheetWidth,
|
||||
Columns: s.thumbColumns,
|
||||
Rows: s.thumbRows,
|
||||
func (s *appState) persistThumbnailConfig() {
|
||||
cfg := thumbnailConfig{
|
||||
ContactSheet: s.thumbnailContactSheet,
|
||||
ShowTimestamps: s.thumbnailShowTimestamps,
|
||||
Count: s.thumbnailCount,
|
||||
Width: s.thumbnailWidth,
|
||||
SheetWidth: s.thumbnailSheetWidth,
|
||||
Columns: s.thumbnailColumns,
|
||||
Rows: s.thumbnailRows,
|
||||
}
|
||||
if err := savePersistedThumbConfig(cfg); err != nil {
|
||||
if err := savePersistedThumbnailConfig(cfg); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to persist thumb config: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,33 +21,33 @@ import (
|
|||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||
)
|
||||
|
||||
func (s *appState) showThumbView() {
|
||||
func (s *appState) showThumbnailView() {
|
||||
s.stopPreview()
|
||||
s.lastModule = s.active
|
||||
s.active = "thumb"
|
||||
if cfg, err := loadPersistedThumbConfig(); err == nil {
|
||||
s.applyThumbConfig(cfg)
|
||||
s.active = "thumbnail"
|
||||
if cfg, err := loadPersistedThumbnailConfig(); err == nil {
|
||||
s.applyThumbnailConfig(cfg)
|
||||
}
|
||||
s.setContent(buildThumbView(s))
|
||||
s.setContent(buildThumbnailView(s))
|
||||
}
|
||||
|
||||
func (s *appState) addThumbSource(src *videoSource) {
|
||||
func (s *appState) addThumbnailSource(src *videoSource) {
|
||||
if src == nil {
|
||||
return
|
||||
}
|
||||
for _, existing := range s.thumbFiles {
|
||||
for _, existing := range s.thumbnailFiles {
|
||||
if existing != nil && existing.Path == src.Path {
|
||||
return
|
||||
}
|
||||
}
|
||||
s.thumbFiles = append(s.thumbFiles, src)
|
||||
s.thumbnailFiles = append(s.thumbnailFiles, src)
|
||||
}
|
||||
|
||||
func (s *appState) loadThumbSourceAtIndex(idx int) {
|
||||
if idx < 0 || idx >= len(s.thumbFiles) {
|
||||
func (s *appState) loadThumbnailSourceAtIndex(idx int) {
|
||||
if idx < 0 || idx >= len(s.thumbnailFiles) {
|
||||
return
|
||||
}
|
||||
current := s.thumbFiles[idx]
|
||||
current := s.thumbnailFiles[idx]
|
||||
if current == nil || current.Path == "" {
|
||||
return
|
||||
}
|
||||
|
|
@ -62,16 +62,16 @@ func (s *appState) loadThumbSourceAtIndex(idx int) {
|
|||
return
|
||||
}
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.thumbFiles[idx] = probed
|
||||
if s.thumbFile != nil && s.thumbFile.Path == path {
|
||||
s.thumbFile = probed
|
||||
s.thumbnailFiles[idx] = probed
|
||||
if s.thumbnailFile != nil && s.thumbnailFile.Path == path {
|
||||
s.thumbnailFile = probed
|
||||
}
|
||||
s.showThumbView()
|
||||
s.showThumbnailView()
|
||||
}, false)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *appState) loadMultipleThumbVideos(paths []string) {
|
||||
func (s *appState) loadMultipleThumbnailVideos(paths []string) {
|
||||
if len(paths) == 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -97,19 +97,19 @@ func (s *appState) loadMultipleThumbVideos(paths []string) {
|
|||
return
|
||||
}
|
||||
|
||||
s.thumbFiles = valid
|
||||
s.thumbFile = valid[0]
|
||||
s.thumbnailFiles = valid
|
||||
s.thumbnailFile = valid[0]
|
||||
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.showThumbView()
|
||||
s.showThumbnailView()
|
||||
if len(failed) > 0 {
|
||||
logging.Debug(logging.CatModule, "%d file(s) failed to analyze: %s", len(failed), strings.Join(failed, ", "))
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
|
||||
func buildThumbView(state *appState) fyne.CanvasObject {
|
||||
thumbColor := moduleColor("thumb")
|
||||
func buildThumbnailView(state *appState) fyne.CanvasObject {
|
||||
thumbColor := moduleColor("thumbnail")
|
||||
|
||||
// Back button
|
||||
backBtn := widget.NewButton("< THUMBNAILS", func() {
|
||||
|
|
@ -137,20 +137,20 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
instructions.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Initialize state defaults
|
||||
if state.thumbCount == 0 {
|
||||
state.thumbCount = 24 // Default to 24 thumbnails (good for contact sheets)
|
||||
if state.thumbnailCount == 0 {
|
||||
state.thumbnailCount = 24 // Default to 24 thumbnails (good for contact sheets)
|
||||
}
|
||||
if state.thumbWidth == 0 {
|
||||
state.thumbWidth = 320
|
||||
if state.thumbnailWidth == 0 {
|
||||
state.thumbnailWidth = 320
|
||||
}
|
||||
if state.thumbSheetWidth == 0 {
|
||||
state.thumbSheetWidth = 360
|
||||
if state.thumbnailSheetWidth == 0 {
|
||||
state.thumbnailSheetWidth = 360
|
||||
}
|
||||
if state.thumbColumns == 0 {
|
||||
state.thumbColumns = 4 // 4 columns works well for widescreen videos
|
||||
if state.thumbnailColumns == 0 {
|
||||
state.thumbnailColumns = 4 // 4 columns works well for widescreen videos
|
||||
}
|
||||
if state.thumbRows == 0 {
|
||||
state.thumbRows = 8 // 4x8 = 32 thumbnails
|
||||
if state.thumbnailRows == 0 {
|
||||
state.thumbnailRows = 8 // 4x8 = 32 thumbnails
|
||||
}
|
||||
|
||||
// File label and video preview
|
||||
|
|
@ -158,12 +158,12 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
var videoContainer fyne.CanvasObject
|
||||
if state.thumbFile != nil && state.thumbFile.Width == 0 && state.thumbFile.Height == 0 {
|
||||
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.thumbFile.Path)))
|
||||
if state.thumbnailFile != nil && state.thumbnailFile.Width == 0 && state.thumbnailFile.Height == 0 {
|
||||
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.thumbnailFile.Path)))
|
||||
videoContainer = container.NewCenter(widget.NewLabel("Loading preview..."))
|
||||
} else if state.thumbFile != nil {
|
||||
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.thumbFile.Path)))
|
||||
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.thumbFile, nil)
|
||||
} else if state.thumbnailFile != nil {
|
||||
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.thumbnailFile.Path)))
|
||||
videoContainer = buildVideoPane(state, fyne.NewSize(480, 270), state.thumbnailFile, nil)
|
||||
} else {
|
||||
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
|
||||
}
|
||||
|
|
@ -183,28 +183,28 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
return
|
||||
}
|
||||
|
||||
state.thumbFile = src
|
||||
state.addThumbSource(src)
|
||||
state.showThumbView()
|
||||
state.thumbnailFile = src
|
||||
state.addThumbnailSource(src)
|
||||
state.showThumbnailView()
|
||||
logging.Debug(logging.CatModule, "loaded thumbnail file: %s", path)
|
||||
}, state.window)
|
||||
})
|
||||
|
||||
// Clear button
|
||||
clearBtn := widget.NewButton("Clear", func() {
|
||||
state.thumbFile = nil
|
||||
state.thumbFiles = nil
|
||||
state.showThumbView()
|
||||
state.thumbnailFile = nil
|
||||
state.thumbnailFiles = nil
|
||||
state.showThumbnailView()
|
||||
})
|
||||
clearBtn.Importance = widget.LowImportance
|
||||
|
||||
// Contact sheet checkbox (wrapped)
|
||||
contactSheetCheck := widget.NewCheck("", func(checked bool) {
|
||||
state.thumbContactSheet = checked
|
||||
state.persistThumbConfig()
|
||||
state.showThumbView()
|
||||
state.thumbnailContactSheet = checked
|
||||
state.persistThumbnailConfig()
|
||||
state.showThumbnailView()
|
||||
})
|
||||
contactSheetCheck.Checked = state.thumbContactSheet
|
||||
contactSheetCheck.Checked = state.thumbnailContactSheet
|
||||
contactSheetLabel := widget.NewLabel("Generate Contact Sheet (single image)")
|
||||
contactSheetLabel.Wrapping = fyne.TextWrapWord
|
||||
contactSheetToggle := ui.NewTappable(contactSheetLabel, func() {
|
||||
|
|
@ -213,10 +213,10 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
contactSheetRow := container.NewBorder(nil, nil, contactSheetCheck, nil, contactSheetToggle)
|
||||
|
||||
timestampCheck := widget.NewCheck("", func(checked bool) {
|
||||
state.thumbShowTimestamps = checked
|
||||
state.persistThumbConfig()
|
||||
state.thumbnailShowTimestamps = checked
|
||||
state.persistThumbnailConfig()
|
||||
})
|
||||
timestampCheck.Checked = state.thumbShowTimestamps
|
||||
timestampCheck.Checked = state.thumbnailShowTimestamps
|
||||
timestampLabel := widget.NewLabel("Show timestamps on thumbnails")
|
||||
timestampLabel.Wrapping = fyne.TextWrapWord
|
||||
timestampToggle := ui.NewTappable(timestampLabel, func() {
|
||||
|
|
@ -226,53 +226,53 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
|
||||
// Conditional settings based on contact sheet mode
|
||||
var settingsOptions fyne.CanvasObject
|
||||
if state.thumbContactSheet {
|
||||
if state.thumbnailContactSheet {
|
||||
// Contact sheet mode: show columns and rows
|
||||
colLabel := widget.NewLabel(fmt.Sprintf("Columns: %d", state.thumbColumns))
|
||||
rowLabel := widget.NewLabel(fmt.Sprintf("Rows: %d", state.thumbRows))
|
||||
colLabel := widget.NewLabel(fmt.Sprintf("Columns: %d", state.thumbnailColumns))
|
||||
rowLabel := widget.NewLabel(fmt.Sprintf("Rows: %d", state.thumbnailRows))
|
||||
|
||||
totalThumbs := state.thumbColumns * state.thumbRows
|
||||
totalThumbs := state.thumbnailColumns * state.thumbnailRows
|
||||
totalLabel := widget.NewLabel(fmt.Sprintf("Total thumbnails: %d", totalThumbs))
|
||||
totalLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
totalLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
colSlider := widget.NewSlider(2, 9)
|
||||
colSlider.Value = float64(state.thumbColumns)
|
||||
colSlider.Value = float64(state.thumbnailColumns)
|
||||
colSlider.Step = 1
|
||||
colSlider.OnChanged = func(val float64) {
|
||||
state.thumbColumns = int(val)
|
||||
state.thumbnailColumns = int(val)
|
||||
colLabel.SetText(fmt.Sprintf("Columns: %d", int(val)))
|
||||
totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbColumns*state.thumbRows))
|
||||
state.persistThumbConfig()
|
||||
totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbnailColumns*state.thumbnailRows))
|
||||
state.persistThumbnailConfig()
|
||||
}
|
||||
|
||||
rowSlider := widget.NewSlider(2, 12)
|
||||
rowSlider.Value = float64(state.thumbRows)
|
||||
rowSlider.Value = float64(state.thumbnailRows)
|
||||
rowSlider.Step = 1
|
||||
rowSlider.OnChanged = func(val float64) {
|
||||
state.thumbRows = int(val)
|
||||
state.thumbnailRows = int(val)
|
||||
rowLabel.SetText(fmt.Sprintf("Rows: %d", int(val)))
|
||||
totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbColumns*state.thumbRows))
|
||||
state.persistThumbConfig()
|
||||
totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbnailColumns*state.thumbnailRows))
|
||||
state.persistThumbnailConfig()
|
||||
}
|
||||
|
||||
sizeOptions := []string{"240 px", "300 px", "360 px", "420 px", "480 px"}
|
||||
sizeSelect := widget.NewSelect(sizeOptions, func(val string) {
|
||||
switch val {
|
||||
case "240 px":
|
||||
state.thumbSheetWidth = 240
|
||||
state.thumbnailSheetWidth = 240
|
||||
case "300 px":
|
||||
state.thumbSheetWidth = 300
|
||||
state.thumbnailSheetWidth = 300
|
||||
case "360 px":
|
||||
state.thumbSheetWidth = 360
|
||||
state.thumbnailSheetWidth = 360
|
||||
case "420 px":
|
||||
state.thumbSheetWidth = 420
|
||||
state.thumbnailSheetWidth = 420
|
||||
case "480 px":
|
||||
state.thumbSheetWidth = 480
|
||||
state.thumbnailSheetWidth = 480
|
||||
}
|
||||
state.persistThumbConfig()
|
||||
state.persistThumbnailConfig()
|
||||
})
|
||||
switch state.thumbSheetWidth {
|
||||
switch state.thumbnailSheetWidth {
|
||||
case 240:
|
||||
sizeSelect.SetSelected("240 px")
|
||||
case 300:
|
||||
|
|
@ -298,24 +298,24 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
)
|
||||
} else {
|
||||
// Individual thumbnails mode: show count and width
|
||||
countLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Count: %d", state.thumbCount))
|
||||
countLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Count: %d", state.thumbnailCount))
|
||||
countSlider := widget.NewSlider(3, 50)
|
||||
countSlider.Value = float64(state.thumbCount)
|
||||
countSlider.Value = float64(state.thumbnailCount)
|
||||
countSlider.Step = 1
|
||||
countSlider.OnChanged = func(val float64) {
|
||||
state.thumbCount = int(val)
|
||||
state.thumbnailCount = int(val)
|
||||
countLabel.SetText(fmt.Sprintf("Thumbnail Count: %d", int(val)))
|
||||
state.persistThumbConfig()
|
||||
state.persistThumbnailConfig()
|
||||
}
|
||||
|
||||
widthLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Width: %d px", state.thumbWidth))
|
||||
widthLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Width: %d px", state.thumbnailWidth))
|
||||
widthSlider := widget.NewSlider(160, 640)
|
||||
widthSlider.Value = float64(state.thumbWidth)
|
||||
widthSlider.Value = float64(state.thumbnailWidth)
|
||||
widthSlider.Step = 32
|
||||
widthSlider.OnChanged = func(val float64) {
|
||||
state.thumbWidth = int(val)
|
||||
state.thumbnailWidth = int(val)
|
||||
widthLabel.SetText(fmt.Sprintf("Thumbnail Width: %d px", int(val)))
|
||||
state.persistThumbConfig()
|
||||
state.persistThumbnailConfig()
|
||||
}
|
||||
|
||||
settingsOptions = container.NewVBox(
|
||||
|
|
@ -330,12 +330,12 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
|
||||
// Helper function to create thumbnail job
|
||||
createThumbJob := func() *queue.Job {
|
||||
return state.createThumbJobForPath(state.thumbFile.Path)
|
||||
return state.createThumbnailJobForPath(state.thumbnailFile.Path)
|
||||
}
|
||||
|
||||
// Generate Now button - adds to queue and starts it
|
||||
generateNowBtn := widget.NewButton("GENERATE NOW", func() {
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
dialog.ShowInformation("No Video", "Please load a video file first.", state.window)
|
||||
return
|
||||
}
|
||||
|
|
@ -358,13 +358,13 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
})
|
||||
generateNowBtn.Importance = widget.HighImportance
|
||||
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
generateNowBtn.Disable()
|
||||
}
|
||||
|
||||
// Add to Queue button
|
||||
addQueueBtn := widget.NewButton("Add to Queue", func() {
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
dialog.ShowInformation("No Video", "Please load a video file first.", state.window)
|
||||
return
|
||||
}
|
||||
|
|
@ -381,12 +381,12 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
})
|
||||
addQueueBtn.Importance = widget.MediumImportance
|
||||
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
addQueueBtn.Disable()
|
||||
}
|
||||
|
||||
addAllBtn := widget.NewButton("Add All to Queue", func() {
|
||||
if len(state.thumbFiles) == 0 {
|
||||
if len(state.thumbnailFiles) == 0 {
|
||||
dialog.ShowInformation("No Videos", "Load videos first to add to queue.", state.window)
|
||||
return
|
||||
}
|
||||
|
|
@ -394,13 +394,13 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
dialog.ShowInformation("Queue", "Queue not initialized.", state.window)
|
||||
return
|
||||
}
|
||||
for _, src := range state.thumbFiles {
|
||||
for _, src := range state.thumbnailFiles {
|
||||
if src == nil || src.Path == "" {
|
||||
continue
|
||||
}
|
||||
state.jobQueue.Add(state.createThumbJobForPath(src.Path))
|
||||
state.jobQueue.Add(state.createThumbnailJobForPath(src.Path))
|
||||
}
|
||||
dialog.ShowInformation("Queue", fmt.Sprintf("Queued %d thumbnail jobs.", len(state.thumbFiles)), state.window)
|
||||
dialog.ShowInformation("Queue", fmt.Sprintf("Queued %d thumbnail jobs.", len(state.thumbnailFiles)), state.window)
|
||||
})
|
||||
addAllBtn.Importance = widget.MediumImportance
|
||||
|
||||
|
|
@ -412,20 +412,20 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
|
||||
// View Results button - shows output folder if it exists
|
||||
viewResultsBtn := widget.NewButton("View Results", func() {
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
dialog.ShowInformation("No Video", "Load a video first to locate results.", state.window)
|
||||
return
|
||||
}
|
||||
|
||||
videoDir := filepath.Dir(state.thumbFile.Path)
|
||||
videoBaseName := strings.TrimSuffix(filepath.Base(state.thumbFile.Path), filepath.Ext(state.thumbFile.Path))
|
||||
videoDir := filepath.Dir(state.thumbnailFile.Path)
|
||||
videoBaseName := strings.TrimSuffix(filepath.Base(state.thumbnailFile.Path), filepath.Ext(state.thumbnailFile.Path))
|
||||
outputDir := filepath.Join(videoDir, fmt.Sprintf("%s_thumbnails", videoBaseName))
|
||||
if state.thumbContactSheet {
|
||||
if state.thumbnailContactSheet {
|
||||
outputDir = videoDir
|
||||
}
|
||||
|
||||
// If contact sheet mode, try to open contact sheet image
|
||||
if state.thumbContactSheet {
|
||||
if state.thumbnailContactSheet {
|
||||
contactSheetPath := filepath.Join(outputDir, fmt.Sprintf("%s_contact_sheet.jpg", videoBaseName))
|
||||
if _, err := os.Stat(contactSheetPath); err == nil {
|
||||
if err := openFile(contactSheetPath); err != nil {
|
||||
|
|
@ -452,9 +452,9 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
// Otherwise, open first thumbnail
|
||||
firstThumb := filepath.Join(outputDir, "thumb_0001.jpg")
|
||||
if _, err := os.Stat(firstThumb); err == nil {
|
||||
if err := openFile(firstThumb); err != nil {
|
||||
firstThumbnail := filepath.Join(outputDir, "thumbnail_0001.jpg")
|
||||
if _, err := os.Stat(firstThumbnail); err == nil {
|
||||
if err := openFile(firstThumbnail); err != nil {
|
||||
dialog.ShowError(fmt.Errorf("failed to open thumbnail: %w", err), state.window)
|
||||
}
|
||||
return
|
||||
|
|
@ -466,7 +466,7 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
})
|
||||
viewResultsBtn.Importance = widget.MediumImportance
|
||||
if state.thumbFile == nil {
|
||||
if state.thumbnailFile == nil {
|
||||
viewResultsBtn.Disable()
|
||||
}
|
||||
|
||||
|
|
@ -487,16 +487,16 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
|
||||
// Main content - split layout with preview on left, settings on right
|
||||
leftColumn := container.NewVBox(videoContainer)
|
||||
if len(state.thumbFiles) > 1 {
|
||||
if len(state.thumbnailFiles) > 1 {
|
||||
list := widget.NewList(
|
||||
func() int { return len(state.thumbFiles) },
|
||||
func() int { return len(state.thumbnailFiles) },
|
||||
func() fyne.CanvasObject { return widget.NewLabel("") },
|
||||
func(i widget.ListItemID, o fyne.CanvasObject) {
|
||||
if i < 0 || i >= len(state.thumbFiles) {
|
||||
if i < 0 || i >= len(state.thumbnailFiles) {
|
||||
return
|
||||
}
|
||||
label := o.(*widget.Label)
|
||||
src := state.thumbFiles[i]
|
||||
src := state.thumbnailFiles[i]
|
||||
if src == nil {
|
||||
label.SetText("")
|
||||
return
|
||||
|
|
@ -505,16 +505,16 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
},
|
||||
)
|
||||
list.OnSelected = func(id widget.ListItemID) {
|
||||
if id < 0 || id >= len(state.thumbFiles) {
|
||||
if id < 0 || id >= len(state.thumbnailFiles) {
|
||||
return
|
||||
}
|
||||
state.thumbFile = state.thumbFiles[id]
|
||||
state.loadThumbSourceAtIndex(id)
|
||||
state.showThumbView()
|
||||
state.thumbnailFile = state.thumbnailFiles[id]
|
||||
state.loadThumbnailSourceAtIndex(id)
|
||||
state.showThumbnailView()
|
||||
}
|
||||
if state.thumbFile != nil {
|
||||
for i, src := range state.thumbFiles {
|
||||
if src != nil && src.Path == state.thumbFile.Path {
|
||||
if state.thumbnailFile != nil {
|
||||
for i, src := range state.thumbnailFiles {
|
||||
if src != nil && src.Path == state.thumbnailFile.Path {
|
||||
list.Select(i)
|
||||
break
|
||||
}
|
||||
|
|
@ -543,7 +543,7 @@ func buildThumbView(state *appState) fyne.CanvasObject {
|
|||
return container.NewBorder(topBar, bottomBar, nil, nil, content)
|
||||
}
|
||||
|
||||
func (s *appState) executeThumbJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||
func (s *appState) executeThumbnailJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||
cfg := job.Config
|
||||
inputPath := cfg["inputPath"].(string)
|
||||
outputDir := cfg["outputDir"].(string)
|
||||
|
|
@ -611,30 +611,30 @@ func (s *appState) executeThumbJob(ctx context.Context, job *queue.Job, progress
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *appState) createThumbJobForPath(path string) *queue.Job {
|
||||
func (s *appState) createThumbnailJobForPath(path string) *queue.Job {
|
||||
videoDir := filepath.Dir(path)
|
||||
videoBaseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
outputDir := filepath.Join(videoDir, fmt.Sprintf("%s_thumbnails", videoBaseName))
|
||||
outputFile := outputDir
|
||||
if s.thumbContactSheet {
|
||||
if s.thumbnailContactSheet {
|
||||
outputDir = videoDir
|
||||
outputFile = filepath.Join(videoDir, fmt.Sprintf("%s_contact_sheet.jpg", videoBaseName))
|
||||
}
|
||||
|
||||
var count, width int
|
||||
var description string
|
||||
if s.thumbContactSheet {
|
||||
count = s.thumbColumns * s.thumbRows
|
||||
width = s.thumbSheetWidth
|
||||
description = fmt.Sprintf("Contact sheet: %dx%d grid (%d thumbnails)", s.thumbColumns, s.thumbRows, count)
|
||||
if s.thumbnailContactSheet {
|
||||
count = s.thumbnailColumns * s.thumbnailRows
|
||||
width = s.thumbnailSheetWidth
|
||||
description = fmt.Sprintf("Contact sheet: %dx%d grid (%d thumbnails)", s.thumbnailColumns, s.thumbnailRows, count)
|
||||
} else {
|
||||
count = s.thumbCount
|
||||
width = s.thumbWidth
|
||||
count = s.thumbnailCount
|
||||
width = s.thumbnailWidth
|
||||
description = fmt.Sprintf("%d individual thumbnails (%dpx width)", count, width)
|
||||
}
|
||||
|
||||
return &queue.Job{
|
||||
Type: queue.JobTypeThumb,
|
||||
Type: queue.JobTypeThumbnail,
|
||||
Title: filepath.Base(path),
|
||||
Description: description,
|
||||
InputFile: path,
|
||||
|
|
@ -644,10 +644,10 @@ func (s *appState) createThumbJobForPath(path string) *queue.Job {
|
|||
"outputDir": outputDir,
|
||||
"count": float64(count),
|
||||
"width": float64(width),
|
||||
"contactSheet": s.thumbContactSheet,
|
||||
"showTimestamp": s.thumbShowTimestamps,
|
||||
"columns": float64(s.thumbColumns),
|
||||
"rows": float64(s.thumbRows),
|
||||
"contactSheet": s.thumbnailContactSheet,
|
||||
"showTimestamp": s.thumbnailShowTimestamps,
|
||||
"columns": float64(s.thumbnailColumns),
|
||||
"rows": float64(s.thumbnailRows),
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user