summaryrefslogtreecommitdiff
path: root/internal/gui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 09:55:02 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 09:55:02 +0300
commitf486e9c677c72a409df8696104661847354286df (patch)
tree8f1b9aef0a74d33a65f1f47f2bdf3c9705fc8474 /internal/gui
parentcd4265b8f87811db758c89bab5e62daa6c39337a (diff)
refactor: registry pattern for audio and image provider factories
Add internal/registry generic Registry[K,T] for keyed factory registration. Wire audio.NewProvider via registered per-provider constructors; GUI and processor newImageSearcher use registries of *Orchestrator/*Processor methods. Export image.ImageProviderOpenAI and ImageProviderNanoBanana from search.go and use them across gui to avoid duplicate string constants. Made-with: Cursor
Diffstat (limited to 'internal/gui')
-rw-r--r--internal/gui/app.go7
-rw-r--r--internal/gui/app_test.go4
-rw-r--r--internal/gui/card_service.go3
-rw-r--r--internal/gui/generator_test.go6
-rw-r--r--internal/gui/orchestrator.go83
5 files changed, 57 insertions, 46 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index f7a7d51..6c1cf21 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -146,11 +146,6 @@ type Config struct {
Translator *translation.Translator
}
-const (
- imageProviderOpenAI = "openai"
- imageProviderNanoBanana = "nanobanana"
-)
-
// DefaultConfig returns default GUI configuration
func DefaultConfig() *Config {
homeDir, err := appconfig.HomeDir()
@@ -168,7 +163,7 @@ func DefaultConfig() *Config {
NanoBananaModel: image.DefaultNanoBananaModel,
NanoBananaTextModel: image.DefaultNanoBananaTextModel,
GeminiTTSModel: audioDefaults.GeminiTTSModel,
- ImageProvider: imageProviderNanoBanana,
+ ImageProvider: image.ImageProviderNanoBanana,
TranslationProvider: translation.ProviderGemini,
PhoneticProvider: phonetic.ProviderGemini,
AutoPlay: true, // Auto-play enabled by default
diff --git a/internal/gui/app_test.go b/internal/gui/app_test.go
index 9ac0125..86e8ec2 100644
--- a/internal/gui/app_test.go
+++ b/internal/gui/app_test.go
@@ -19,8 +19,8 @@ func TestDefaultConfigUsesGeminiLanguageProviders(t *testing.T) {
if config.PhoneticProvider != phonetic.ProviderGemini {
t.Fatalf("DefaultConfig() phonetic provider = %q, want %q", config.PhoneticProvider, phonetic.ProviderGemini)
}
- if config.ImageProvider != imageProviderNanoBanana {
- t.Fatalf("DefaultConfig() image provider = %q, want %q", config.ImageProvider, imageProviderNanoBanana)
+ if config.ImageProvider != image.ImageProviderNanoBanana {
+ t.Fatalf("DefaultConfig() image provider = %q, want %q", config.ImageProvider, image.ImageProviderNanoBanana)
}
if config.AudioProvider != audioDefaults.Provider {
t.Fatalf("DefaultConfig() audio provider = %q, want %q", config.AudioProvider, audioDefaults.Provider)
diff --git a/internal/gui/card_service.go b/internal/gui/card_service.go
index 395c741..cd4d549 100644
--- a/internal/gui/card_service.go
+++ b/internal/gui/card_service.go
@@ -9,6 +9,7 @@ import (
"codeberg.org/snonux/totalrecall/internal"
"codeberg.org/snonux/totalrecall/internal/anki"
+ "codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/store"
)
@@ -249,7 +250,7 @@ func (cs *CardService) loadImageFile(wordDir string, cf *CardFiles) {
// Try to load the image prompt from the attribution file as a fallback
// when the image provider is AI-based (OpenAI DALL-E or Nano Banana).
- if cs.config.ImageProvider == imageProviderOpenAI || cs.config.ImageProvider == imageProviderNanoBanana {
+ if cs.config.ImageProvider == image.ImageProviderOpenAI || cs.config.ImageProvider == image.ImageProviderNanoBanana {
cs.loadPromptFromAttribution(cf)
}
}
diff --git a/internal/gui/generator_test.go b/internal/gui/generator_test.go
index 40c0e2c..5ee238b 100644
--- a/internal/gui/generator_test.go
+++ b/internal/gui/generator_test.go
@@ -34,7 +34,7 @@ func (f *fakePromptAwareImageClient) Search(_ context.Context, opts *image.Searc
Height: 1,
Description: "fake result",
Attribution: "fake attribution",
- Source: imageProviderNanoBanana,
+ Source: image.ImageProviderNanoBanana,
},
}, nil
}
@@ -48,7 +48,7 @@ func (f *fakePromptAwareImageClient) GetAttribution(*image.SearchResult) string
}
func (f *fakePromptAwareImageClient) Name() string {
- return imageProviderNanoBanana
+ return image.ImageProviderNanoBanana
}
func (f *fakePromptAwareImageClient) SetPromptCallback(callback func(prompt string)) {
@@ -99,7 +99,7 @@ func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) {
tempDir := t.TempDir()
app := &Application{
config: &Config{
- ImageProvider: imageProviderNanoBanana,
+ ImageProvider: image.ImageProviderNanoBanana,
GoogleAPIKey: "google-key",
NanoBananaModel: "custom-image-model",
NanoBananaTextModel: "custom-text-model",
diff --git a/internal/gui/orchestrator.go b/internal/gui/orchestrator.go
index 191e919..ef4a167 100644
--- a/internal/gui/orchestrator.go
+++ b/internal/gui/orchestrator.go
@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"time"
"fyne.io/fyne/v2"
@@ -12,6 +13,7 @@ import (
"codeberg.org/snonux/totalrecall/internal/audio"
"codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/phonetic"
+ "codeberg.org/snonux/totalrecall/internal/registry"
"codeberg.org/snonux/totalrecall/internal/translation"
)
@@ -410,48 +412,61 @@ func (o *GenerationOrchestrator) imagePromptCallback(cardDir, word string) func(
}
}
-// newImageSearcher constructs the appropriate image client based on the
-// configured image provider. Returns image.PromptAwareClient so callers can
-// call SetPromptCallback directly without a type-assertion. The factory
-// functions are sourced from imageFactories (the shared image.ClientFactories
-// value) to avoid duplicating the factory signatures in this package.
-func (o *GenerationOrchestrator) newImageSearcher() (image.PromptAwareClient, error) {
- switch o.config.ImageProvider {
- case imageProviderOpenAI:
- if o.config.OpenAIKey == "" {
- return nil, fmt.Errorf("OpenAI API key is required for image generation")
- }
+// guiImageClientFactories maps provider name to image client builder. Add new
+// providers by registering here instead of extending a switch in newImageSearcher.
+var guiImageClientFactories = func() *registry.Registry[string, func(*GenerationOrchestrator) (image.PromptAwareClient, error)] {
+ r := registry.New[string, func(*GenerationOrchestrator) (image.PromptAwareClient, error)]()
+ r.Register(image.ImageProviderOpenAI, (*GenerationOrchestrator).buildOpenAIImageClient)
+ r.Register(image.ImageProviderNanoBanana, (*GenerationOrchestrator).buildNanoBananaImageClient)
+ return r
+}()
- openaiConfig := &image.OpenAIConfig{
- APIKey: o.config.OpenAIKey,
- Model: "dall-e-2", // DALL-E 2 supports 512×512
- Size: "512x512",
- Quality: "standard",
- Style: "natural",
- }
+func (o *GenerationOrchestrator) buildOpenAIImageClient() (image.PromptAwareClient, error) {
+ if o.config.OpenAIKey == "" {
+ return nil, fmt.Errorf("OpenAI API key is required for image generation")
+ }
- return o.imageFactories.NewOpenAIClient(openaiConfig), nil
+ openaiConfig := &image.OpenAIConfig{
+ APIKey: o.config.OpenAIKey,
+ Model: "dall-e-2", // DALL-E 2 supports 512×512
+ Size: "512x512",
+ Quality: "standard",
+ Style: "natural",
+ }
- case imageProviderNanoBanana:
- cfg := o.config
- if cfg == nil {
- cfg = DefaultConfig()
- }
- if cfg.GoogleAPIKey == "" {
- return nil, fmt.Errorf("google API key is required for image generation")
- }
+ return o.imageFactories.NewOpenAIClient(openaiConfig), nil
+}
- nanoBananaConfig := &image.NanoBananaConfig{
- APIKey: cfg.GoogleAPIKey,
- Model: cfg.NanoBananaModel,
- TextModel: cfg.NanoBananaTextModel,
- }
+func (o *GenerationOrchestrator) buildNanoBananaImageClient() (image.PromptAwareClient, error) {
+ cfg := o.config
+ if cfg == nil {
+ cfg = DefaultConfig()
+ }
+ if cfg.GoogleAPIKey == "" {
+ return nil, fmt.Errorf("google API key is required for image generation")
+ }
- return o.imageFactories.NewNanoBananaClient(nanoBananaConfig), nil
+ nanoBananaConfig := &image.NanoBananaConfig{
+ APIKey: cfg.GoogleAPIKey,
+ Model: cfg.NanoBananaModel,
+ TextModel: cfg.NanoBananaTextModel,
+ }
+
+ return o.imageFactories.NewNanoBananaClient(nanoBananaConfig), nil
+}
- default:
+// newImageSearcher constructs the appropriate image client based on the
+// configured image provider. Returns image.PromptAwareClient so callers can
+// call SetPromptCallback directly without a type-assertion. The factory
+// functions are sourced from imageFactories (the shared image.ClientFactories
+// value) to avoid duplicating the factory signatures in this package.
+func (o *GenerationOrchestrator) newImageSearcher() (image.PromptAwareClient, error) {
+ key := strings.ToLower(strings.TrimSpace(o.config.ImageProvider))
+ fn, ok := guiImageClientFactories.Get(key)
+ if !ok {
return nil, fmt.Errorf("unknown image provider: %s", o.config.ImageProvider)
}
+ return fn(o)
}
// --- Phonetics ---