diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-18 15:13:29 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-18 15:13:29 +0300 |
| commit | 93500a6d9a237c3a78a73d552c17583938672a59 (patch) | |
| tree | 99a16d84877ea7753db7350df4c1b60bf14467c1 | |
| parent | 1b3228d4c93f5951a1d6cc0e77a364c3dde3e625 (diff) | |
fix: improve APKG export functionalityv0.4.0
- Fix Anki import errors by adding missing deck fields (newToday, revToday, etc.)
- Fix media files not displaying/playing by using original filenames in cards
- Add phonetic information to notes field with proper line breaks
- Add 'e' hotkey for quick APKG export
- Increment version to 0.4.0
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | internal/anki/apkg_generator.go | 89 | ||||
| -rw-r--r-- | internal/anki/generator.go | 8 | ||||
| -rw-r--r-- | internal/gui/app.go | 5 | ||||
| -rw-r--r-- | internal/version.go | 2 |
4 files changed, 66 insertions, 38 deletions
diff --git a/internal/anki/apkg_generator.go b/internal/anki/apkg_generator.go index 099c146..6f0043f 100644 --- a/internal/anki/apkg_generator.go +++ b/internal/anki/apkg_generator.go @@ -52,15 +52,8 @@ func (g *APKGGenerator) GenerateAPKG(outputPath string) error { } defer os.RemoveAll(tempDir) - // Create SQLite database - dbPath := filepath.Join(tempDir, "collection.anki2") - if err := g.createDatabase(dbPath); err != nil { - return fmt.Errorf("failed to create database: %w", err) - } - - // Copy media files - mediaDir := filepath.Join(tempDir, "media") - if err := g.copyMediaFiles(mediaDir); err != nil { + // Copy media files FIRST (this populates g.mediaFiles map) + if err := g.copyMediaFiles(tempDir); err != nil { return fmt.Errorf("failed to copy media files: %w", err) } @@ -69,6 +62,12 @@ func (g *APKGGenerator) GenerateAPKG(outputPath string) error { return fmt.Errorf("failed to create media mapping: %w", err) } + // Create SQLite database (this uses g.mediaFiles map) + dbPath := filepath.Join(tempDir, "collection.anki2") + if err := g.createDatabase(dbPath); err != nil { + return fmt.Errorf("failed to create database: %w", err) + } + // Create the .apkg zip file if err := g.createZipPackage(tempDir, outputPath); err != nil { return fmt.Errorf("failed to create zip package: %w", err) @@ -194,26 +193,41 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error { now := time.Now().Unix() // Create deck configuration + // The arrays are [learningCount, reviewCount] for today's stats decks := map[string]interface{}{ "1": map[string]interface{}{ - "id": 1, - "name": "Default", - "mod": now, - "desc": "", - "collapsed": false, - "dyn": 0, - "conf": 1, - "usn": 0, + "id": 1, + "name": "Default", + "mod": now, + "desc": "", + "collapsed": false, + "dyn": 0, + "conf": 1, + "usn": 0, + "newToday": []int{0, 0}, + "revToday": []int{0, 0}, + "lrnToday": []int{0, 0}, + "timeToday": []int{0, 0}, + "browserCollapsed": false, + "extendNew": 10, + "extendRev": 50, }, fmt.Sprintf("%d", g.deckID): map[string]interface{}{ - "id": g.deckID, - "name": g.deckName, - "mod": now, - "desc": "Bulgarian vocabulary cards created by TotalRecall", - "collapsed": false, - "dyn": 0, - "conf": 1, - "usn": 0, + "id": g.deckID, + "name": g.deckName, + "mod": now, + "desc": "Bulgarian vocabulary cards created by TotalRecall", + "collapsed": false, + "dyn": 0, + "conf": 1, + "usn": 0, + "newToday": []int{0, 0}, + "revToday": []int{0, 0}, + "lrnToday": []int{0, 0}, + "timeToday": []int{0, 0}, + "browserCollapsed": false, + "extendNew": 10, + "extendRev": 50, }, } decksJSON, _ := json.Marshal(decks) @@ -489,16 +503,20 @@ func (g *APKGGenerator) insertNotesAndCards(db *sql.DB) error { } imageField := "" - if card.ImageFile != "" { - if num, ok := g.mediaFiles[filepath.Base(card.ImageFile)]; ok { - imageField = fmt.Sprintf(`<img src="%d">`, num) + if card.ImageFile != "" && fileExists(card.ImageFile) { + basename := filepath.Base(card.ImageFile) + if _, ok := g.mediaFiles[basename]; ok { + // Use the original filename in the card content + imageField = fmt.Sprintf(`<img src="%s">`, basename) } } audioField := "" - if card.AudioFile != "" { - if num, ok := g.mediaFiles[filepath.Base(card.AudioFile)]; ok { - audioField = fmt.Sprintf("[sound:%d]", num) + if card.AudioFile != "" && fileExists(card.AudioFile) { + basename := filepath.Base(card.AudioFile) + if _, ok := g.mediaFiles[basename]; ok { + // Use the original filename in the card content + audioField = fmt.Sprintf("[sound:%s]", basename) } } @@ -564,16 +582,15 @@ func (g *APKGGenerator) insertNotesAndCards(db *sql.DB) error { } // copyMediaFiles copies media files and assigns them numbers -func (g *APKGGenerator) copyMediaFiles(mediaDir string) error { - // Media files don't go in a subdirectory for .apkg - // They go directly in the temp directory with numeric names +func (g *APKGGenerator) copyMediaFiles(tempDir string) error { + // Media files go directly in the temp directory with numeric names for _, card := range g.cards { // Copy audio file if card.AudioFile != "" && fileExists(card.AudioFile) { filename := filepath.Base(card.AudioFile) if _, exists := g.mediaFiles[filename]; !exists { - targetPath := filepath.Join(filepath.Dir(mediaDir), fmt.Sprintf("%d", g.mediaCounter)) + targetPath := filepath.Join(tempDir, fmt.Sprintf("%d", g.mediaCounter)) if err := copyFile(card.AudioFile, targetPath); err != nil { return fmt.Errorf("failed to copy audio file %s: %w", card.AudioFile, err) } @@ -586,7 +603,7 @@ func (g *APKGGenerator) copyMediaFiles(mediaDir string) error { if card.ImageFile != "" && fileExists(card.ImageFile) { filename := filepath.Base(card.ImageFile) if _, exists := g.mediaFiles[filename]; !exists { - targetPath := filepath.Join(filepath.Dir(mediaDir), fmt.Sprintf("%d", g.mediaCounter)) + targetPath := filepath.Join(tempDir, fmt.Sprintf("%d", g.mediaCounter)) if err := copyFile(card.ImageFile, targetPath); err != nil { return fmt.Errorf("failed to copy image file %s: %w", card.ImageFile, err) } diff --git a/internal/anki/generator.go b/internal/anki/generator.go index eecce48..21b1995 100644 --- a/internal/anki/generator.go +++ b/internal/anki/generator.go @@ -192,6 +192,14 @@ func (g *Generator) GenerateFromDirectory(dir string) error { } } + // Load phonetic information as notes + phoneticFile := filepath.Join(wordDir, fmt.Sprintf("%s_phonetic.txt", sanitizedWord)) + if data, err := os.ReadFile(phoneticFile); err == nil { + // Preserve line breaks by converting \n to <br> for HTML display + notes := strings.TrimSpace(string(data)) + card.Notes = strings.ReplaceAll(notes, "\n", "<br>") + } + // Only add card if it has at least some content if card.AudioFile != "" || card.ImageFile != "" || card.Translation != "" { g.AddCard(card) diff --git a/internal/gui/app.go b/internal/gui/app.go index b84a917..4a392ed 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -334,7 +334,7 @@ func (a *Application) setupUI() { // Create menu fileMenu := fyne.NewMenu("File", - fyne.NewMenuItem("Export to Anki...", a.onExportToAnki), + fyne.NewMenuItem("Export to Anki... (E)", a.onExportToAnki), fyne.NewMenuItemSeparator(), fyne.NewMenuItem("Preferences...", a.onPreferences), fyne.NewMenuItemSeparator(), @@ -1439,6 +1439,9 @@ func (a *Application) handleShortcutKey(key fyne.KeyName) { if a.currentAudioFile != "" { a.audioPlayer.Play() } + + case fyne.KeyE: // Export to APKG + a.onExportToAnki() } } diff --git a/internal/version.go b/internal/version.go index 547370b..1dee17c 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,3 @@ package internal -const Version = "0.3.0" +const Version = "0.4.0" |
