summaryrefslogtreecommitdiff
path: root/internal/gui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-06 11:12:03 +0300
committerPaul Buetow <paul@buetow.org>2026-04-06 11:12:03 +0300
commit616beecc41b573503dad9f5bfd9f353c6f826a8a (patch)
tree3189ae3f048dfc4e8ff79b83caab8ea43c2d7492 /internal/gui
parent05f54cc0cb8cf3535698ab5027d200842bdb28e3 (diff)
refactor: extract shared CardStore into internal/store to eliminate duplication
FindCardDirectory, FindOrCreateCardDirectory, GenerateCardID and the ScanWords helper previously existed in both internal/utils.go (as standalone functions) and were partially duplicated in internal/gui/card_service.go (readWordFromDir, ScanExistingWords). Introduce internal/store.CardStore as the single source of truth for all on-disk card-directory operations. Both internal/processor and internal/gui now hold a *store.CardStore field and delegate to it, removing the last copy of the directory-scanning loop from card_service.go. internal/utils.go keeps thin forwarding wrappers for callers that import the root internal package. Also adds table-driven unit tests for the new package covering FindCardDirectory (including legacy _word.txt fallback), FindOrCreateCardDirectory, and CardStore.ScanWords. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/gui')
-rw-r--r--internal/gui/card_service.go88
1 files changed, 20 insertions, 68 deletions
diff --git a/internal/gui/card_service.go b/internal/gui/card_service.go
index 38108ca..395c741 100644
--- a/internal/gui/card_service.go
+++ b/internal/gui/card_service.go
@@ -4,47 +4,48 @@ import (
"fmt"
"os"
"path/filepath"
- "sort"
"strings"
"time"
"codeberg.org/snonux/totalrecall/internal"
"codeberg.org/snonux/totalrecall/internal/anki"
+ "codeberg.org/snonux/totalrecall/internal/store"
)
// CardService manages card file discovery, directory creation, persistence,
// and state loading from the output directory. It is responsible for all
// non-UI file I/O related to cards, decoupled from UI event-wiring.
+// Directory-scanning and creation are delegated to a store.CardStore so the
+// underlying algorithm is shared with the processor package (DRY).
type CardService struct {
- config *Config
+ config *Config
+ cardStore *store.CardStore
}
// NewCardService constructs a CardService for the given configuration.
+// It initialises an internal store.CardStore rooted at config.OutputDir.
func NewCardService(config *Config) *CardService {
- return &CardService{config: config}
+ return &CardService{
+ config: config,
+ cardStore: store.New(config.OutputDir),
+ }
}
// FindCardDirectory finds the directory for a given Bulgarian word.
-// Delegates to the shared internal.FindCardDirectory which also handles the
-// legacy _word.txt fallback for backward compatibility.
+// Delegates to the shared store.CardStore which also handles the legacy
+// _word.txt fallback for backward compatibility.
func (cs *CardService) FindCardDirectory(word string) string {
- return internal.FindCardDirectory(cs.config.OutputDir, word)
+ return cs.cardStore.FindCardDirectory(word)
}
// EnsureWordDirectoryAndMetadata creates a new card directory and writes word
// metadata to word.txt inside it. Returns the directory path.
+// Uses store.FindOrCreateCardDirectory so the creation logic is not duplicated.
func (cs *CardService) EnsureWordDirectoryAndMetadata(word string) (string, error) {
- cardID := internal.GenerateCardID(word)
- wordDir := filepath.Join(cs.config.OutputDir, cardID)
- if err := os.MkdirAll(wordDir, 0755); err != nil {
- return "", fmt.Errorf("failed to create card directory: %w", err)
- }
-
- 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)
+ wordDir := cs.cardStore.FindOrCreateCardDirectory(word)
+ if wordDir == "" || wordDir == cs.config.OutputDir {
+ return "", fmt.Errorf("failed to create card directory for %q", word)
}
-
return wordDir, nil
}
@@ -62,59 +63,10 @@ func (cs *CardService) EnsureCardDirectory(word string) (string, error) {
// ScanExistingWords scans the output directory for existing card subdirectories
// and returns a sorted list of the Bulgarian words found. A directory counts
// only if it contains at least one of: an audio file, an image, or a
-// translation file.
+// translation file. Delegates iteration and word-file reading to the shared
+// store.CardStore so that logic is not duplicated here.
func (cs *CardService) ScanExistingWords() []string {
- words := []string{}
-
- entries, err := os.ReadDir(cs.config.OutputDir)
- if err != nil {
- // Directory doesn't exist yet; return empty list silently.
- return words
- }
-
- // Each subdirectory represents a card identified by a card ID.
- for _, entry := range entries {
- if !entry.IsDir() {
- continue
- }
-
- cardID := entry.Name()
- wordDir := filepath.Join(cs.config.OutputDir, cardID)
-
- word, ok := cs.readWordFromDir(wordDir)
- if !ok {
- continue
- }
-
- if cs.dirHasContent(wordDir) {
- words = append(words, word)
- }
- }
-
- sort.Strings(words)
- return words
-}
-
-// readWordFromDir reads the Bulgarian word from word.txt (or the legacy
-// _word.txt) inside a card directory. Returns the word and true on success.
-func (cs *CardService) readWordFromDir(wordDir string) (string, bool) {
- wordFile := filepath.Join(wordDir, "word.txt")
- wordData, err := os.ReadFile(wordFile)
- if err != nil {
- // Try old format with underscore for backward compatibility.
- wordFile = filepath.Join(wordDir, "_word.txt")
- wordData, err = os.ReadFile(wordFile)
- if err != nil {
- return "", false
- }
- }
-
- word := string(wordData)
- if word == "" {
- return "", false
- }
-
- return word, true
+ return cs.cardStore.ScanWords(cs.dirHasContent)
}
// dirHasContent returns true if the card directory contains at least one audio