diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-06 11:12:03 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-06 11:12:03 +0300 |
| commit | 616beecc41b573503dad9f5bfd9f353c6f826a8a (patch) | |
| tree | 3189ae3f048dfc4e8ff79b83caab8ea43c2d7492 /internal/gui | |
| parent | 05f54cc0cb8cf3535698ab5027d200842bdb28e3 (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.go | 88 |
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 |
