summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-22 08:15:07 +0300
committerPaul Buetow <paul@buetow.org>2025-07-22 08:15:07 +0300
commit35ff0f508cf9cb52048f2ed67bd07f4aec17f2bf (patch)
tree3c3aef2d478e3edde300311137df43b2353af5f2 /internal
parent1cc15cfbd68d45ae6d561e5659422e72bf9ecd1d (diff)
Fix GUI background image generation race conditions
- Add file.Sync() after image download to ensure data is flushed to disk - Add double-checking in UI updates to prevent wrong card updates - Fix background job completion to reload files when user navigates back - Add file size validation in image display widget - Improve error messages for image loading failures This fixes two issues: 1. 'png: invalid format: not enough pixel data' error when navigating during generation 2. Images not updating when navigating back to a card after background generation completes
Diffstat (limited to 'internal')
-rw-r--r--internal/gui/app.go77
-rw-r--r--internal/gui/widgets.go19
-rw-r--r--internal/image/download.go82
3 files changed, 128 insertions, 50 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 6a58286..685c913 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -716,11 +716,19 @@ func (a *Application) generateMaterials(word string) {
a.mu.Lock()
if a.currentWord == word {
a.currentAudioFile = audioRes.file
+ audioFile := audioRes.file
+ a.mu.Unlock()
fyne.Do(func() {
- a.audioPlayer.SetAudioFile(audioRes.file)
+ // Double-check inside the UI update that we're still on the same word
+ a.mu.Lock()
+ if a.currentWord == word {
+ a.audioPlayer.SetAudioFile(audioFile)
+ }
+ a.mu.Unlock()
})
+ } else {
+ a.mu.Unlock()
}
- a.mu.Unlock()
}
// Collect image result
@@ -735,11 +743,19 @@ func (a *Application) generateMaterials(word string) {
a.mu.Lock()
if a.currentWord == word {
a.currentImage = imageRes.file
+ imageFile := imageRes.file
+ a.mu.Unlock()
fyne.Do(func() {
- a.imageDisplay.SetImages([]string{imageRes.file})
+ // Double-check inside the UI update that we're still on the same word
+ a.mu.Lock()
+ if a.currentWord == word {
+ a.imageDisplay.SetImages([]string{imageFile})
+ }
+ a.mu.Unlock()
})
+ } else {
+ a.mu.Unlock()
}
- a.mu.Unlock()
}
// Collect phonetic result (UI already updated in the goroutine)
@@ -967,19 +983,34 @@ func (a *Application) onRegenerateAudio() {
defer a.wg.Done()
defer a.decrementProcessing() // Audio processing ends
+ // Store the word we're generating for
+ wordForGeneration := a.currentWord
+
// Get or create context for this card
- cardCtx, _ := a.getOrCreateCardContext(a.currentWord)
+ cardCtx, _ := a.getOrCreateCardContext(wordForGeneration)
- audioFile, err := a.generateAudio(cardCtx, a.currentWord)
+ audioFile, err := a.generateAudio(cardCtx, wordForGeneration)
if err != nil {
fyne.Do(func() {
a.showError(fmt.Errorf("Audio regeneration failed: %w", err))
})
} else {
- a.currentAudioFile = audioFile
- fyne.Do(func() {
- a.audioPlayer.SetAudioFile(audioFile)
- })
+ // Only update if we're still on the same word
+ a.mu.Lock()
+ if a.currentWord == wordForGeneration {
+ a.currentAudioFile = audioFile
+ a.mu.Unlock()
+ fyne.Do(func() {
+ // Double-check inside the UI update that we're still on the same word
+ a.mu.Lock()
+ if a.currentWord == wordForGeneration {
+ a.audioPlayer.SetAudioFile(audioFile)
+ }
+ a.mu.Unlock()
+ })
+ } else {
+ a.mu.Unlock()
+ }
}
fyne.Do(func() {
@@ -1897,6 +1928,14 @@ func (a *Application) processWordJob(job *WordJob) {
if isCurrentJob {
fyne.Do(func() {
+ // Double-check that we're still on the same job before updating UI
+ a.mu.Lock()
+ if a.currentJobID != job.ID {
+ a.mu.Unlock()
+ return
+ }
+ a.mu.Unlock()
+
a.audioPlayer.SetAudioFile(audioFile)
// Enable audio-related actions
a.regenerateAudioBtn.Enable()
@@ -1948,6 +1987,14 @@ func (a *Application) processWordJob(job *WordJob) {
if isCurrentJob {
fyne.Do(func() {
+ // Double-check that we're still on the same job before updating UI
+ a.mu.Lock()
+ if a.currentJobID != job.ID {
+ a.mu.Unlock()
+ return
+ }
+ a.mu.Unlock()
+
a.translationEntry.SetText(translation)
if imageFile != "" {
a.imageDisplay.SetImages([]string{imageFile})
@@ -2022,6 +2069,16 @@ func (a *Application) onJobComplete(job *WordJob) {
} else {
// This is a background job that completed
a.updateStatus(fmt.Sprintf("Background processing completed: %s", job.Word))
+
+ // Check if user has navigated back to this word
+ a.mu.Lock()
+ currentWord := a.currentWord
+ a.mu.Unlock()
+
+ if currentWord == job.Word {
+ // User is currently viewing this word, reload the files
+ a.loadExistingFiles(job.Word)
+ }
}
}
})
diff --git a/internal/gui/widgets.go b/internal/gui/widgets.go
index 6ad428c..df74027 100644
--- a/internal/gui/widgets.go
+++ b/internal/gui/widgets.go
@@ -73,7 +73,20 @@ func (d *ImageDisplay) SetImage(imagePath string) {
}
defer file.Close()
- img, _, err := image.Decode(file)
+ // Get file info to ensure it's fully written
+ stat, err := file.Stat()
+ if err != nil {
+ d.imageLabel.SetText(fmt.Sprintf("Error getting file info: %v", err))
+ return
+ }
+
+ // If file size is 0, it might still be writing
+ if stat.Size() == 0 {
+ d.imageLabel.SetText("Image file is empty")
+ return
+ }
+
+ img, format, err := image.Decode(file)
if err != nil {
d.imageLabel.SetText(fmt.Sprintf("Error decoding image: %v", err))
return
@@ -83,8 +96,8 @@ func (d *ImageDisplay) SetImage(imagePath string) {
d.imageCanvas.Image = img
d.imageCanvas.Refresh()
- // Update label
- d.imageLabel.SetText(filepath.Base(imagePath))
+ // Update label with format info
+ d.imageLabel.SetText(fmt.Sprintf("%s (%s)", filepath.Base(imagePath), format))
}
// SetImages sets multiple images but only displays the first one
diff --git a/internal/image/download.go b/internal/image/download.go
index fabb1bb..264ad1a 100644
--- a/internal/image/download.go
+++ b/internal/image/download.go
@@ -11,21 +11,21 @@ import (
// DownloadOptions configures image download behavior
type DownloadOptions struct {
- OutputDir string // Directory to save images
+ OutputDir string // Directory to save images
OverwriteExisting bool // Whether to overwrite existing files
- CreateDir bool // Create output directory if it doesn't exist
- FileNamePattern string // Pattern for file naming (e.g., "{word}_{source}")
- MaxSizeBytes int64 // Maximum file size to download (0 = no limit)
+ CreateDir bool // Create output directory if it doesn't exist
+ FileNamePattern string // Pattern for file naming (e.g., "{word}_{source}")
+ MaxSizeBytes int64 // Maximum file size to download (0 = no limit)
}
// DefaultDownloadOptions returns sensible defaults for image downloads
func DefaultDownloadOptions() *DownloadOptions {
return &DownloadOptions{
- OutputDir: "./images",
+ OutputDir: "./images",
OverwriteExisting: false,
- CreateDir: true,
- FileNamePattern: "{word}_{source}",
- MaxSizeBytes: 10 * 1024 * 1024, // 10MB
+ CreateDir: true,
+ FileNamePattern: "{word}_{source}",
+ MaxSizeBytes: 10 * 1024 * 1024, // 10MB
}
}
@@ -55,28 +55,28 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou
return fmt.Errorf("failed to create directory: %w", err)
}
}
-
+
// Check if file already exists
if !d.options.OverwriteExisting {
if _, err := os.Stat(outputPath); err == nil {
return fmt.Errorf("file already exists: %s", outputPath)
}
}
-
+
// Download the image
reader, err := d.searcher.Download(ctx, result.URL)
if err != nil {
return fmt.Errorf("failed to download image: %w", err)
}
defer reader.Close()
-
+
// Create output file
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
-
+
// Copy with size limit if specified
var written int64
if d.options.MaxSizeBytes > 0 {
@@ -85,7 +85,7 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou
os.Remove(outputPath) // Clean up on error
return fmt.Errorf("failed to write file: %w", err)
}
-
+
// Check if we hit the size limit
if written == d.options.MaxSizeBytes {
// Try to read one more byte to see if file is larger
@@ -94,6 +94,11 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou
return fmt.Errorf("image exceeds maximum size of %d bytes", d.options.MaxSizeBytes)
}
}
+
+ // Ensure the file is fully written to disk before returning
+ if err := file.Sync(); err != nil {
+ return fmt.Errorf("failed to sync file to disk: %w", err)
+ }
} else {
written, err = io.Copy(file, reader)
if err != nil {
@@ -101,7 +106,12 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou
return fmt.Errorf("failed to write file: %w", err)
}
}
-
+
+ // Ensure the file is fully written to disk before returning
+ if err := file.Sync(); err != nil {
+ return fmt.Errorf("failed to sync file to disk: %w", err)
+ }
+
// Save attribution if required
if attribution := d.searcher.GetAttribution(result); attribution != "" {
attrPath := strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + "_attribution.txt"
@@ -110,7 +120,7 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou
fmt.Fprintf(os.Stderr, "Warning: failed to save attribution: %v\n", err)
}
}
-
+
return nil
}
@@ -119,32 +129,32 @@ func (d *Downloader) DownloadBestMatch(ctx context.Context, query string) (*Sear
// Search for images
opts := DefaultSearchOptions(query)
opts.PerPage = 5 // Get top 5 results
-
+
results, err := d.searcher.Search(ctx, opts)
if err != nil {
return nil, "", fmt.Errorf("search failed: %w", err)
}
-
+
if len(results) == 0 {
return nil, "", fmt.Errorf("no images found for query: %s", query)
}
-
+
// Try to download the first available image
for i, result := range results {
// Generate filename
filename := d.generateFileName(query, &result, i)
outputPath := filepath.Join(d.options.OutputDir, filename)
-
+
// Try to download
err := d.DownloadImage(ctx, &result, outputPath)
if err == nil {
return &result, outputPath, nil
}
-
+
// Log error and try next
fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err)
}
-
+
return nil, "", fmt.Errorf("failed to download any images for query: %s", query)
}
@@ -152,24 +162,24 @@ func (d *Downloader) DownloadBestMatch(ctx context.Context, query string) (*Sear
func (d *Downloader) generateFileName(word string, result *SearchResult, index int) string {
// Start with the pattern
filename := d.options.FileNamePattern
-
+
// Replace placeholders
filename = strings.ReplaceAll(filename, "{word}", sanitizeFileName(word))
filename = strings.ReplaceAll(filename, "{source}", result.Source)
filename = strings.ReplaceAll(filename, "{id}", result.ID)
filename = strings.ReplaceAll(filename, "{index}", fmt.Sprintf("%d", index))
-
+
// Determine extension from URL
ext := filepath.Ext(result.URL)
if ext == "" || len(ext) > 5 { // Probably not a real extension
ext = ".jpg" // Default to jpg
}
-
+
// Add extension if not present
if filepath.Ext(filename) == "" {
filename += ext
}
-
+
return filename
}
@@ -189,49 +199,47 @@ func sanitizeFileName(name string) string {
" ", "_",
".", "_",
)
-
+
sanitized := replacer.Replace(name)
-
+
// Ensure the filename is not too long
if len(sanitized) > 50 {
sanitized = sanitized[:50]
}
-
+
return sanitized
}
-
// DownloadBestMatchWithOptions downloads the best matching image for given search options
func (d *Downloader) DownloadBestMatchWithOptions(ctx context.Context, opts *SearchOptions) (*SearchResult, string, error) {
// Search for images
- searchOpts := *opts // Copy to avoid modifying original
+ searchOpts := *opts // Copy to avoid modifying original
searchOpts.PerPage = 5 // Get top 5 results
-
+
results, err := d.searcher.Search(ctx, &searchOpts)
if err != nil {
return nil, "", fmt.Errorf("search failed: %w", err)
}
-
+
if len(results) == 0 {
return nil, "", fmt.Errorf("no images found for query: %s", opts.Query)
}
-
+
// Try to download the first available image
for i, result := range results {
// Generate filename
filename := d.generateFileName(opts.Query, &result, i)
outputPath := filepath.Join(d.options.OutputDir, filename)
-
+
// Try to download
err := d.DownloadImage(ctx, &result, outputPath)
if err == nil {
return &result, outputPath, nil
}
-
+
// Log error and try next
fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err)
}
-
+
return nil, "", fmt.Errorf("failed to download any images for query: %s", opts.Query)
}
-