summaryrefslogtreecommitdiff
path: root/internal/gui
diff options
context:
space:
mode:
Diffstat (limited to 'internal/gui')
-rw-r--r--internal/gui/app.go210
-rw-r--r--internal/gui/generator.go58
-rw-r--r--internal/gui/navigation.go12
-rw-r--r--internal/gui/queue.go67
4 files changed, 223 insertions, 124 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 547a22b..f776d9b 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -93,6 +93,10 @@ type Application struct {
// Per-card cancellation tracking
cardContexts map[string]context.CancelFunc // Map of word -> cancel function
cardMu sync.Mutex // Mutex for cardContexts map
+
+ // Active operations tracking
+ activeOperations map[string]int // Map of word -> count of active operations
+ activeOpMu sync.Mutex // Mutex for activeOperations map
}
// Config holds GUI application configuration
@@ -147,13 +151,14 @@ func New(config *Config) *Application {
myApp.SetIcon(GetAppIcon())
app := &Application{
- app: myApp,
- config: config,
- ctx: ctx,
- cancel: cancel,
- savedCards: make([]anki.Card, 0),
- cardContexts: make(map[string]context.CancelFunc),
- autoPlayEnabled: config.AutoPlay, // Use config setting
+ app: myApp,
+ config: config,
+ ctx: ctx,
+ cancel: cancel,
+ savedCards: make([]anki.Card, 0),
+ cardContexts: make(map[string]context.CancelFunc),
+ activeOperations: make(map[string]int),
+ autoPlayEnabled: config.AutoPlay, // Use config setting
}
// Initialize the word processing queue
@@ -556,6 +561,16 @@ func (a *Application) onSubmit() {
func (a *Application) generateMaterials(word string) {
// Get or create context for this card
cardCtx, _ := a.getOrCreateCardContext(word)
+
+ // Ensure card directory exists
+ cardDir, err := a.ensureCardDirectory(word)
+ if err != nil {
+ fyne.Do(func() {
+ a.showError(fmt.Errorf("Failed to create card directory: %w", err))
+ a.setUIEnabled(true)
+ })
+ return
+ }
// Check if we already have a translation
if a.currentTranslation == "" {
// Translate word
@@ -580,25 +595,13 @@ func (a *Application) generateMaterials(word string) {
}
a.mu.Unlock()
- // Save translation to disk regardless
+ // Save translation to disk using the pre-determined directory
if translation != "" {
- // Find existing card directory first
- wordDir := a.findCardDirectory(word)
- if wordDir == "" {
- // No existing directory, create new one with card ID
- cardID := internal.GenerateCardID(word)
- wordDir = filepath.Join(a.config.OutputDir, cardID)
- os.MkdirAll(wordDir, 0755) // Ensure directory exists
- // Save word metadata
- metadataFile := filepath.Join(wordDir, "word.txt")
- os.WriteFile(metadataFile, []byte(word), 0644)
- }
- translationFile := filepath.Join(wordDir, "translation.txt")
+ translationFile := filepath.Join(cardDir, "translation.txt")
content := fmt.Sprintf("%s = %s\n", word, translation)
os.WriteFile(translationFile, []byte(content), 0644)
}
}
-
// Create channels for parallel operations
type audioResult struct {
file string
@@ -634,11 +637,14 @@ func (a *Application) generateMaterials(word string) {
// 1. Audio generation
go func() {
+ a.startOperation(word) // Track operation start
+ defer a.endOperation(word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Audio processing starts
})
- audioFile, err := a.generateAudio(cardCtx, word)
+ audioFile, err := a.generateAudio(cardCtx, word, cardDir)
a.decrementProcessing() // Audio processing ends
audioChan <- audioResult{file: audioFile, err: err}
@@ -646,6 +652,9 @@ func (a *Application) generateMaterials(word string) {
// 2. Image generation
go func() {
+ a.startOperation(word) // Track operation start
+ defer a.endOperation(word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Image processing starts
// Show generating status if this is still the current word
@@ -656,7 +665,7 @@ func (a *Application) generateMaterials(word string) {
a.mu.Unlock()
})
- imageFile, err := a.generateImagesWithPrompt(cardCtx, word, customPrompt, translation)
+ imageFile, err := a.generateImagesWithPrompt(cardCtx, word, customPrompt, translation, cardDir)
a.decrementProcessing() // Image processing ends
imageChan <- imageResult{file: imageFile, err: err}
@@ -664,6 +673,9 @@ func (a *Application) generateMaterials(word string) {
// 3. Phonetic information fetching
go func() {
+ a.startOperation(word) // Track operation start
+ defer a.endOperation(word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Phonetic processing starts
})
@@ -677,11 +689,11 @@ func (a *Application) generateMaterials(word string) {
fmt.Printf("Successfully fetched phonetic info for '%s': %s\n", word, phoneticInfo)
}
- // Save phonetic info to disk
+ // Save phonetic info to disk using the pre-determined directory
if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" {
- a.savePhoneticInfoForWord(word, phoneticInfo)
+ phoneticFile := filepath.Join(cardDir, "phonetic.txt")
+ os.WriteFile(phoneticFile, []byte(phoneticInfo), 0644)
}
-
// Update UI immediately with phonetic info if this is still the current word
if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" {
a.mu.Lock()
@@ -879,7 +891,16 @@ func (a *Application) onRegenerateImage() {
// Get or create context for this card
cardCtx, _ := a.getOrCreateCardContext(wordForGeneration)
- imageFile, err := a.generateImagesWithPrompt(cardCtx, wordForGeneration, customPrompt, translation)
+ // Ensure card directory exists
+ cardDir, err := a.ensureCardDirectory(wordForGeneration)
+ if err != nil {
+ fyne.Do(func() {
+ a.showError(fmt.Errorf("Failed to create card directory: %w", err))
+ })
+ return
+ }
+
+ imageFile, err := a.generateImagesWithPrompt(cardCtx, wordForGeneration, customPrompt, translation, cardDir)
if err != nil {
fyne.Do(func() {
a.showError(fmt.Errorf("Image regeneration failed: %w", err))
@@ -943,7 +964,16 @@ func (a *Application) onRegenerateRandomImage() {
// Get or create context for this card
cardCtx, _ := a.getOrCreateCardContext(wordForGeneration)
- imageFile, err := a.generateImagesWithPrompt(cardCtx, wordForGeneration, customPrompt, translation)
+ // Ensure card directory exists
+ cardDir, err := a.ensureCardDirectory(wordForGeneration)
+ if err != nil {
+ fyne.Do(func() {
+ a.showError(fmt.Errorf("Failed to create card directory: %w", err))
+ })
+ return
+ }
+
+ imageFile, err := a.generateImagesWithPrompt(cardCtx, wordForGeneration, customPrompt, translation, cardDir)
if err != nil {
fyne.Do(func() {
a.showError(fmt.Errorf("Random image generation failed: %w", err))
@@ -986,15 +1016,33 @@ func (a *Application) onRegenerateAudio() {
a.wg.Add(1)
go func() {
defer a.wg.Done()
- defer a.decrementProcessing() // Audio processing ends
+ defer a.decrementProcessing() // Image processing ends
+ // Use the current translation to avoid re-translating
+ translation := a.currentTranslation
+ if translation == "" {
+ // Use the text from translationEntry if currentTranslation is not set
+ translation = strings.TrimSpace(a.translationEntry.Text)
+ }
// Store the word we're generating for
wordForGeneration := a.currentWord
+ a.startOperation(wordForGeneration) // Track operation start
+ defer a.endOperation(wordForGeneration) // Track operation end
+
// Get or create context for this card
cardCtx, _ := a.getOrCreateCardContext(wordForGeneration)
- audioFile, err := a.generateAudio(cardCtx, wordForGeneration)
+ // Ensure card directory exists
+ cardDir, err := a.ensureCardDirectory(wordForGeneration)
+ if err != nil {
+ fyne.Do(func() {
+ a.showError(fmt.Errorf("Failed to create card directory: %w", err))
+ })
+ return
+ }
+
+ audioFile, err := a.generateAudio(cardCtx, wordForGeneration, cardDir)
if err != nil {
fyne.Do(func() {
a.showError(fmt.Errorf("Audio regeneration failed: %w", err))
@@ -1725,6 +1773,30 @@ func (a *Application) getOrCreateCardContext(word string) (context.Context, cont
return ctx, cancel
}
+// ensureCardDirectory ensures a card directory exists for the given word and returns its path
+func (a *Application) ensureCardDirectory(word string) (string, error) {
+ // First check if directory already exists
+ wordDir := a.findCardDirectory(word)
+ if wordDir != "" {
+ return wordDir, nil
+ }
+
+ // Create new directory with card ID
+ cardID := internal.GenerateCardID(word)
+ wordDir = filepath.Join(a.config.OutputDir, cardID)
+ if err := os.MkdirAll(wordDir, 0755); err != nil {
+ return "", fmt.Errorf("failed to create word directory: %w", err)
+ }
+
+ // Save the original Bulgarian word in a metadata file
+ metadataFile := filepath.Join(wordDir, "word.txt")
+ if err := os.WriteFile(metadataFile, []byte(word), 0644); err != nil {
+ return "", fmt.Errorf("failed to save word metadata: %w", err)
+ }
+
+ return wordDir, nil
+}
+
// cancelCardOperations cancels all ongoing operations for a specific word
func (a *Application) cancelCardOperations(word string) {
a.cardMu.Lock()
@@ -1736,6 +1808,36 @@ func (a *Application) cancelCardOperations(word string) {
}
}
+// startOperation marks the start of an operation for a word
+func (a *Application) startOperation(word string) {
+ a.activeOpMu.Lock()
+ defer a.activeOpMu.Unlock()
+ a.activeOperations[word]++
+}
+
+// endOperation marks the end of an operation for a word
+func (a *Application) endOperation(word string) {
+ a.activeOpMu.Lock()
+ defer a.activeOpMu.Unlock()
+
+ if count, exists := a.activeOperations[word]; exists {
+ if count > 1 {
+ a.activeOperations[word]--
+ } else {
+ delete(a.activeOperations, word)
+ }
+ }
+}
+
+// hasActiveOperations checks if a word has any active operations
+func (a *Application) hasActiveOperations(word string) bool {
+ a.activeOpMu.Lock()
+ defer a.activeOpMu.Unlock()
+
+ count, exists := a.activeOperations[word]
+ return exists && count > 0
+}
+
// processWordJob processes a single word job
func (a *Application) processWordJob(job *WordJob) {
// Get or create context for this card
@@ -1749,6 +1851,15 @@ func (a *Application) processWordJob(job *WordJob) {
return
default:
}
+
+ // Ensure card directory exists upfront
+ cardDir, dirErr := a.ensureCardDirectory(job.Word)
+ if dirErr != nil {
+ a.queue.FailJob(job.ID, fmt.Errorf("failed to create card directory: %w", dirErr))
+ a.finishCurrentJob()
+ return
+ }
+
// Handle translation
var translation string
var err error
@@ -1772,18 +1883,7 @@ func (a *Application) processWordJob(job *WordJob) {
// Save translation to disk immediately for this specific word
if translation != "" {
- // Find existing card directory first
- wordDir := a.findCardDirectory(job.Word)
- if wordDir == "" {
- // No existing directory, create new one with card ID
- cardID := internal.GenerateCardID(job.Word)
- wordDir = filepath.Join(a.config.OutputDir, cardID)
- os.MkdirAll(wordDir, 0755) // Ensure directory exists
- // Save word metadata
- metadataFile := filepath.Join(wordDir, "word.txt")
- os.WriteFile(metadataFile, []byte(job.Word), 0644)
- }
- translationFile := filepath.Join(wordDir, "translation.txt")
+ translationFile := filepath.Join(cardDir, "translation.txt")
content := fmt.Sprintf("%s = %s\n", job.Word, translation)
os.WriteFile(translationFile, []byte(content), 0644)
}
@@ -1825,11 +1925,14 @@ func (a *Application) processWordJob(job *WordJob) {
// 1. Audio generation
go func() {
+ a.startOperation(job.Word) // Track operation start
+ defer a.endOperation(job.Word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Audio processing starts
})
- audioFile, err := a.generateAudio(cardCtx, job.Word)
+ audioFile, err := a.generateAudio(cardCtx, job.Word, cardDir)
a.decrementProcessing() // Audio processing ends
audioChan <- audioResult{file: audioFile, err: err}
@@ -1837,6 +1940,9 @@ func (a *Application) processWordJob(job *WordJob) {
// 2. Image generation (includes scene description)
go func() {
+ a.startOperation(job.Word) // Track operation start
+ defer a.endOperation(job.Word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Image processing starts
// Show generating status if this is still the current job
@@ -1849,7 +1955,7 @@ func (a *Application) processWordJob(job *WordJob) {
// Use the custom prompt from the job
// The translation variable already contains the correct translation (either from job or translated)
- imageFile, err := a.generateImagesWithPrompt(cardCtx, job.Word, job.CustomPrompt, translation)
+ imageFile, err := a.generateImagesWithPrompt(cardCtx, job.Word, job.CustomPrompt, translation, cardDir)
a.decrementProcessing() // Image processing ends
imageChan <- imageResult{file: imageFile, err: err}
@@ -1857,6 +1963,9 @@ func (a *Application) processWordJob(job *WordJob) {
// 3. Phonetic information fetching
go func() {
+ a.startOperation(job.Word) // Track operation start
+ defer a.endOperation(job.Word) // Track operation end
+
fyne.Do(func() {
a.incrementProcessing() // Phonetic processing starts
})
@@ -1872,18 +1981,7 @@ func (a *Application) processWordJob(job *WordJob) {
// Save phonetic info to disk immediately for this specific word
if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" {
- // Find existing card directory first
- wordDir := a.findCardDirectory(job.Word)
- if wordDir == "" {
- // No existing directory, create new one with card ID
- cardID := internal.GenerateCardID(job.Word)
- wordDir = filepath.Join(a.config.OutputDir, cardID)
- os.MkdirAll(wordDir, 0755) // Ensure directory exists
- // Save word metadata
- metadataFile := filepath.Join(wordDir, "word.txt")
- os.WriteFile(metadataFile, []byte(job.Word), 0644)
- }
- phoneticFile := filepath.Join(wordDir, "phonetic.txt")
+ phoneticFile := filepath.Join(cardDir, "phonetic.txt")
os.WriteFile(phoneticFile, []byte(phoneticInfo), 0644)
}
diff --git a/internal/gui/generator.go b/internal/gui/generator.go
index 814e578..cfb8ad3 100644
--- a/internal/gui/generator.go
+++ b/internal/gui/generator.go
@@ -12,7 +12,6 @@ import (
"fyne.io/fyne/v2"
"github.com/sashabaranov/go-openai"
- "codeberg.org/snonux/totalrecall/internal"
"codeberg.org/snonux/totalrecall/internal/audio"
"codeberg.org/snonux/totalrecall/internal/image"
)
@@ -84,12 +83,11 @@ func (a *Application) translateEnglishToBulgarian(word string) (string, error) {
}
// generateAudio generates audio for a word
-func (a *Application) generateAudio(ctx context.Context, word string) (string, error) {
+func (a *Application) generateAudio(ctx context.Context, word string, cardDir string) (string, error) {
// Check if this is a regeneration by looking for existing audio file
- wordDir := a.findCardDirectory(word)
isRegeneration := false
- if wordDir != "" {
- audioFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", a.config.AudioFormat))
+ if cardDir != "" {
+ audioFile := filepath.Join(cardDir, fmt.Sprintf("audio.%s", a.config.AudioFormat))
if _, err := os.Stat(audioFile); err == nil {
isRegeneration = true
}
@@ -124,25 +122,13 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e
return "", err
}
- // Find existing card directory or create new one again after provider creation
- wordDir = a.findCardDirectory(word)
- if wordDir == "" {
- // No existing directory, create new one with card ID
- cardID := internal.GenerateCardID(word)
- wordDir = filepath.Join(a.config.OutputDir, cardID)
- if err := os.MkdirAll(wordDir, 0755); err != nil {
- return "", fmt.Errorf("failed to create word directory: %w", err)
- }
-
- // Save the original Bulgarian word in a metadata file
- metadataFile := filepath.Join(wordDir, "word.txt")
- if err := os.WriteFile(metadataFile, []byte(word), 0644); err != nil {
- return "", fmt.Errorf("failed to save word metadata: %w", err)
- }
+ // Use the provided card directory
+ if cardDir == "" {
+ return "", fmt.Errorf("card directory not provided")
}
// Generate filename in subdirectory
- outputFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", a.config.AudioFormat))
+ outputFile := filepath.Join(cardDir, fmt.Sprintf("audio.%s", a.config.AudioFormat))
// Generate audio
err = provider.GenerateAudio(ctx, word, outputFile)
@@ -157,7 +143,7 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e
}
// Save voice metadata for GUI display
- metadataFile := filepath.Join(wordDir, "audio_metadata.txt")
+ metadataFile := filepath.Join(cardDir, "audio_metadata.txt")
metadata := fmt.Sprintf("voice=%s\nspeed=%.2f\n", voice, speed)
if err := os.WriteFile(metadataFile, []byte(metadata), 0644); err != nil {
fmt.Printf("Warning: Failed to save audio metadata: %v\n", err)
@@ -167,12 +153,12 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e
}
// generateImages downloads images for a word
-func (a *Application) generateImages(ctx context.Context, word string) (string, error) {
- return a.generateImagesWithPrompt(ctx, word, "", "")
+func (a *Application) generateImages(ctx context.Context, word string, cardDir string) (string, error) {
+ return a.generateImagesWithPrompt(ctx, word, "", "", cardDir)
}
// generateImagesWithPrompt downloads a single image for a word with optional custom prompt and translation
-func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, customPrompt string, translation string) (string, error) {
+func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, customPrompt string, translation string, cardDir string) (string, error) {
// Create image searcher based on provider
var searcher image.ImageSearcher
var err error
@@ -196,26 +182,14 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string,
return "", fmt.Errorf("unknown image provider: %s", a.config.ImageProvider)
}
- // Find existing card directory or create new one
- wordDir := a.findCardDirectory(word)
- if wordDir == "" {
- // No existing directory, create new one with card ID
- cardID := internal.GenerateCardID(word)
- wordDir = filepath.Join(a.config.OutputDir, cardID)
- if err := os.MkdirAll(wordDir, 0755); err != nil {
- return "", fmt.Errorf("failed to create word directory: %w", err)
- }
-
- // Save the original Bulgarian word in a metadata file
- metadataFile := filepath.Join(wordDir, "word.txt")
- if err := os.WriteFile(metadataFile, []byte(word), 0644); err != nil {
- return "", fmt.Errorf("failed to save word metadata: %w", err)
- }
+ // Use the provided card directory
+ if cardDir == "" {
+ return "", fmt.Errorf("card directory not provided")
}
// Create downloader
downloadOpts := &image.DownloadOptions{
- OutputDir: wordDir,
+ OutputDir: cardDir,
OverwriteExisting: true,
CreateDir: true,
FileNamePattern: "image",
@@ -229,7 +203,7 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string,
if openaiClient, ok := searcher.(*image.OpenAIClient); ok {
openaiClient.SetPromptCallback(func(prompt string) {
// Save the prompt to disk immediately for this word
- promptFile := filepath.Join(wordDir, "image_prompt.txt")
+ promptFile := filepath.Join(cardDir, "image_prompt.txt")
os.WriteFile(promptFile, []byte(prompt), 0644)
// Only update UI if this word is still the current word
diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go
index c91a57a..d7410f1 100644
--- a/internal/gui/navigation.go
+++ b/internal/gui/navigation.go
@@ -580,6 +580,18 @@ func (a *Application) onDelete() {
return
}
+ // Check if this word has active operations
+ if a.hasActiveOperations(a.currentWord) {
+ dialog.ShowError(fmt.Errorf("Cannot delete '%s' while content is being generated.\nPlease wait for generation to complete.", a.currentWord), a.window)
+ return
+ }
+
+ // Also check if word is in the processing queue
+ if a.queue.IsWordProcessing(a.currentWord) {
+ dialog.ShowError(fmt.Errorf("Cannot delete '%s' while it is in the processing queue.\nPlease wait for processing to complete.", a.currentWord), a.window)
+ return
+ }
+
// Create custom confirmation dialog with keyboard support
message := fmt.Sprintf("Move all files for '%s' to trash?\n\nPress y to confirm or n to cancel", a.currentWord)
confirmDialog := dialog.NewConfirm("Move to Trash", message, func(confirm bool) {
diff --git a/internal/gui/queue.go b/internal/gui/queue.go
index f72b059..1c52c9f 100644
--- a/internal/gui/queue.go
+++ b/internal/gui/queue.go
@@ -53,14 +53,14 @@ type WordQueue struct {
results map[int]*WordJob
processing map[int]*WordJob
completed []*WordJob
-
- nextID int
- mu sync.RWMutex
-
+
+ nextID int
+ mu sync.RWMutex
+
// Callbacks for UI updates
onStatusUpdate func(job *WordJob)
onJobComplete func(job *WordJob)
-
+
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
@@ -69,7 +69,7 @@ type WordQueue struct {
// NewWordQueue creates a new word processing queue
func NewWordQueue(ctx context.Context) *WordQueue {
queueCtx, cancel := context.WithCancel(ctx)
-
+
q := &WordQueue{
jobs: make(chan *WordJob, 100),
results: make(map[int]*WordJob),
@@ -79,9 +79,9 @@ func NewWordQueue(ctx context.Context) *WordQueue {
ctx: queueCtx,
cancel: cancel,
}
-
+
// Don't start a worker - the GUI will pull jobs
-
+
return q
}
@@ -110,7 +110,7 @@ func (q *WordQueue) AddWordWithPrompt(word, customPrompt string) *WordJob {
q.nextID++
q.results[job.ID] = job
q.mu.Unlock()
-
+
// Try to add to queue
select {
case q.jobs <- job:
@@ -134,7 +134,7 @@ func (q *WordQueue) GetJob(id int) *WordJob {
func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int) {
q.mu.RLock()
defer q.mu.RUnlock()
-
+
// Count based on job statuses for accuracy
for _, job := range q.results {
switch job.Status {
@@ -148,7 +148,7 @@ func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int)
failed++
}
}
-
+
return
}
@@ -156,14 +156,14 @@ func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int)
func (q *WordQueue) GetActiveJobs() []*WordJob {
q.mu.RLock()
defer q.mu.RUnlock()
-
+
var jobs []*WordJob
-
+
// Add processing jobs
for _, job := range q.processing {
jobs = append(jobs, job)
}
-
+
// Add queued jobs from channel (non-blocking)
queuedJobs := make([]*WordJob, 0)
for {
@@ -198,17 +198,17 @@ func (q *WordQueue) Stop() {
func (q *WordQueue) CompleteJob(jobID int, translation, audioFile, imageFile string) {
q.mu.Lock()
defer q.mu.Unlock()
-
+
if job, exists := q.results[jobID]; exists {
job.Status = StatusCompleted
job.Translation = translation
job.AudioFile = audioFile
job.ImageFile = imageFile
job.CompletedAt = time.Now()
-
+
delete(q.processing, jobID)
q.completed = append(q.completed, job)
-
+
if q.onJobComplete != nil {
q.onJobComplete(job)
}
@@ -219,14 +219,14 @@ func (q *WordQueue) CompleteJob(jobID int, translation, audioFile, imageFile str
func (q *WordQueue) FailJob(jobID int, err error) {
q.mu.Lock()
defer q.mu.Unlock()
-
+
if job, exists := q.results[jobID]; exists {
job.Status = StatusFailed
job.Error = err
job.CompletedAt = time.Now()
-
+
delete(q.processing, jobID)
-
+
if q.onJobComplete != nil {
q.onJobComplete(job)
}
@@ -250,14 +250,14 @@ func (q *WordQueue) ProcessNextJob() *WordJob {
job.Status = StatusProcessing
job.StartedAt = time.Now()
q.mu.Unlock()
-
+
// Call the status update callback
if q.onStatusUpdate != nil {
q.onStatusUpdate(job)
}
-
+
return job
-
+
default:
return nil
}
@@ -267,7 +267,7 @@ func (q *WordQueue) ProcessNextJob() *WordJob {
func (q *WordQueue) RemoveCompletedJobByWord(word string) {
q.mu.Lock()
defer q.mu.Unlock()
-
+
// Remove from completed jobs list
newCompleted := make([]*WordJob, 0, len(q.completed))
for _, job := range q.completed {
@@ -276,11 +276,26 @@ func (q *WordQueue) RemoveCompletedJobByWord(word string) {
}
}
q.completed = newCompleted
-
+
// Also remove from results map
for id, job := range q.results {
if job.Word == word && job.Status == StatusCompleted {
delete(q.results, id)
}
}
-} \ No newline at end of file
+}
+
+// IsWordProcessing checks if a word is currently being processed or queued
+func (q *WordQueue) IsWordProcessing(word string) bool {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+
+ // Check all jobs in results
+ for _, job := range q.results {
+ if job.Word == word && (job.Status == StatusQueued || job.Status == StatusProcessing) {
+ return true
+ }
+ }
+
+ return false
+}