Skip to content

feat(lyrics): export editor output to standalone .lrc / .txt file#255

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/lyrics-editor-export-standalone
Jun 16, 2026
Merged

feat(lyrics): export editor output to standalone .lrc / .txt file#255
InstaZDLL merged 2 commits into
mainfrom
feat/lyrics-editor-export-standalone

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Closes #201.

Summary

Adds a third save option to the lyrics editor: write the serialized payload to an arbitrary on-disk path picked through the OS-native save dialog. The two existing options (embed in audio file tag, DB cache only) stay available. Users who want the song's tag block kept clean can now ship the LRC/TXT as a sidecar next to the audio file, the convention every offline player (foobar2000, Musicolet, Spotify-style readers) already understands.

Backend

  • New `export_lyrics_to_path` Tauri command in `commands/lyrics.rs`: validates the parent dir exists, writes via `spawn_blocking + std::fs::write`. UTF-8 without BOM.
  • Registered in `lib.rs`.

Frontend

  • New `exportLyricsToPath` TS wrapper.
  • Extracted `buildPayload` helper from `handleSave` so the new `handleExportToFile` shares the plain / line-synced / word-synced dispatch.
  • New `Save to file…` button in the editor footer (lucide `FileDown` icon) opens `@tauri-apps/plugin-dialog`'s `save()` with sensible defaults:
    • Anchored at the song's parent directory
    • Suggested filename = song basename + `.lrc` for synced output, `.txt` for plain
    • User can switch extensions through the dialog filter dropdown
  • `LyricsPanel` passes `currentTrack?.file_path` as the new `trackFilePath` prop.
  • Helpers `filenameStem` + `defaultExportPath`: audio basename preferred, sanitized track title as fallback, literal `"lyrics"` last resort.

i18n

Three new `lyricsEditor.{exportToFile, exportToFileHint, exportedToFile}` keys propagated natively to all 17 locales.

Test plan

  • `cargo check --workspace --all-targets` clean
  • `cargo test --workspace` β€” 351 passing (baseline preserved)
  • `bun typecheck` + `bun lint` clean
  • Manual: open the lyrics editor, click "Save to file…" β†’ verify dialog opens next to song with `.lrc` as default filename
  • Manual: switch to plain mode, click "Save to file…" β†’ verify `.txt` is the default extension
  • Manual: pick a path, save, verify file content matches the editor payload + audio file tag is untouched

Summary by CodeRabbit

Release Notes

  • Nouvelles FonctionnalitΓ©s

    • Ajout d’un export des paroles via β€œSave to file…”, au choix en .lrc ou .txt.
    • Propose automatiquement un chemin et un nom de fichier basΓ©s sur la piste en cours (lorsque disponible).
    • Applique le dΓ©calage d’aperΓ§u aux timestamps lors de la sauvegarde.
    • Affiche un message de confirmation avec le chemin complet aprΓ¨s export rΓ©ussi.
  • Localisation

    • Mise Γ  jour des traductions i18n pour la nouvelle fonctionnalitΓ© d’export des paroles dans toutes les langues supportΓ©es.

Closes #201.

Adds a third save option to the lyrics editor: write the
serialized payload to an arbitrary on-disk path picked through
the OS-native save dialog. Complements the two existing options
(embed in audio file tag, DB cache only) β€” users who want the
song's tag block kept clean can now ship the LRC/TXT as a
sidecar next to the audio file, the convention every offline
player (foobar2000, Musicolet, Spotify-style readers) already
understands.

## Backend

- New Tauri command `export_lyrics_to_path(target_path,
  content)` in `commands/lyrics.rs`: validates the parent dir
  exists, writes the bytes via `tokio::task::spawn_blocking +
  std::fs::write`. UTF-8 without BOM because LRCLIB / Musicolet
  / smaller offline players all read BOM-less files fine and a
  BOM trips some up.
- Registered in `lib.rs` alongside the existing `save_lyrics` /
  `import_lrc_file` handlers.

## Frontend

- `src/lib/tauri/lyrics.ts`: `exportLyricsToPath(targetPath,
  content)` wrapper.
- `LyricsEditorModal`: extracted the content-serialization
  branch out of `handleSave` into a shared `buildPayload`
  helper so the new `handleExportToFile` doesn't have to
  re-implement the plain/line/word dispatch.
- New `Save to file…` button in the footer next to `Save`,
  uses lucide `FileDown` icon. Opens
  `@tauri-apps/plugin-dialog`'s `save()` with sensible
  defaults: anchors at the song's parent directory with the
  song's basename as the filename stem + `.lrc` for synced
  output, `.txt` for plain. User can switch extensions in the
  dialog filter dropdown.
- `LyricsPanel` passes `currentTrack?.file_path` through as the
  new `trackFilePath` prop so the dialog can land next to the
  song.
- Helpers `filenameStem` + `defaultExportPath`: prefer the
  audio basename, fall back to a sanitized track title, then
  the literal `"lyrics"` so the dialog is never blank. Title
  sanitization strips the characters every common OS rejects
  (`/\:*?"<>|`) and collapses whitespace.

## i18n

Three new keys under `lyricsEditor.*` propagated natively to
all 17 locales:

- `exportToFile` β€” button label (e.g. fr: "Enregistrer dans
  un fichier…")
- `exportToFileHint` β€” tooltip explaining the affordance
  doesn't touch the audio file
- `exportedToFile` β€” success status (`{{path}}` interpolation)

## Validation

- `cargo check --workspace --all-targets` clean
- `cargo test --workspace` β€” 351 passing (baseline preserved)
- `bun typecheck` + `bun lint` clean
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bc22b317-a0b3-4361-9290-288dc79169b0

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between a03c5a5 and 804e119.

πŸ“’ Files selected for processing (2)
  • src-tauri/crates/app/src/commands/lyrics.rs
  • src/components/common/LyricsEditorModal.tsx

πŸ“ Walkthrough

Walkthrough

Ajout d'une fonctionnalitΓ© d'export des paroles vers un fichier .lrc ou .txt indΓ©pendant. Une nouvelle commande Tauri export_lyrics_to_path Γ©crit le contenu sur le disque avec validation du rΓ©pertoire parent. LyricsEditorModal reΓ§oit trackFilePath, expose buildPayload() pour centraliser la sΓ©rialisation (avec dΓ©calage global appliquΓ© aux timestamps), ouvre un dialogue de sauvegarde Tauri et appelle le bridge TS. Les traductions couvrent 18 locales.

Changes

Export des paroles vers un fichier

Layer / File(s) Summary
Commande Tauri backend + enregistrement
src-tauri/crates/app/src/commands/lyrics.rs, src-tauri/crates/app/src/lib.rs
export_lyrics_to_path valide que le chemin cible possède un parent et que le répertoire parent existe, puis écrit le contenu via spawn_blocking, propage les erreurs AppError, et est enregistrée dans tauri::generate_handler!.
Bridge TypeScript
src/lib/tauri/lyrics.ts
exportLyricsToPath(targetPath, content) invoque la commande Tauri export_lyrics_to_path et retourne Promise<void>, avec JSDoc documentant la revalidation backend du parent.
LyricsEditorModal : props, payload, export handler, helpers et bouton
src/components/common/LyricsEditorModal.tsx
Ajout de trackFilePath en prop optionnelle, factorisation de buildPayload() pour centraliser la sΓ©rialisation avec application du dΓ©calage global globalOffsetMs aux timestamps, handleExportToFile qui appelle showSaveDialog avec filtres .lrc/.txt puis exportLyricsToPath, helpers filenameStem()/defaultExportPath() pour ancrer le rΓ©pertoire par dΓ©faut au dossier parent du mΓ©dia, et bouton FileDown dans le footer.
CΓ’blage LyricsPanel
src/components/layout/LyricsPanel.tsx
Passage de currentTrack?.file_path ?? null comme prop trackFilePath Γ  LyricsEditorModal.
Traductions (18 locales)
src/i18n/locales/*.json
Ajout des clΓ©s exportToFile (intitulΓ© du bouton), exportToFileHint (description d'export en .lrc/.txt sans modification du fichier audio), et exportedToFile (confirmation avec interpolation {{path}}) dans lyricsEditor pour ar, de, en, es, fr, hi, id, it, ja, ko, nl, pt-BR, pt, ru, tr, zh-CN, zh-TW.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant LyricsEditorModal
  participant showSaveDialog
  participant exportLyricsToPath
  participant TauriBackend
  participant Filesystem

  User->>LyricsEditorModal: Clique sur le bouton "Export to file…"
  LyricsEditorModal->>LyricsEditorModal: buildPayload() β†’ { content, format }
  LyricsEditorModal->>LyricsEditorModal: defaultExportPath(trackFilePath) β†’ defaultPath
  LyricsEditorModal->>showSaveDialog: { defaultPath, filtres (.lrc, .txt) }
  showSaveDialog-->>LyricsEditorModal: filePath | null
  LyricsEditorModal->>exportLyricsToPath: invoke("export_lyrics_to_path", { targetPath, content })
  exportLyricsToPath->>TauriBackend: export_lyrics_to_path(target_path, content)
  TauriBackend->>Filesystem: spawn_blocking β†’ fs::write(path, content)
  Filesystem-->>TauriBackend: Ok(())
  TauriBackend-->>LyricsEditorModal: Ok(())
  LyricsEditorModal-->>User: Warning toast "Exported to {{path}}"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🎡 Les paroles s'échappent enfin du cache caché,
En .lrc ou .txt, librement exportΓ©es,
Un dialogue s'ouvre, le chemin est tracΓ©,
Le backend valide, le fichier est posΓ©.
Fini les tags souillΓ©s β€” la musique est sauvΓ©e ! 🎢

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Title check βœ… Passed Le titre dΓ©crit fidΓ¨lement la fonctionnalitΓ© principale : ajout d'une option d'export pour sauvegarder les paroles en fichiers autonomes .lrc ou .txt.
Description check βœ… Passed La description est complΓ¨te : elle couvre le rΓ©sumΓ© (pourquoi), les changements backend et frontend dΓ©taillΓ©s, l'i18n, et un plan de test exhaustif avec validations manuelles.
Linked Issues check βœ… Passed Le PR rΓ©pond complΓ¨tement aux exigences de l'issue #201 : ajout d'une troisiΓ¨me option de sauvegarde des paroles en fichier autonome .lrc/.txt, permettant aux utilisateurs de conserver les tags audio intacts et de partager les fichiers sur LRCLIB.
Out of Scope Changes check βœ… Passed Tous les changements (backend Tauri, wrapper TS, composant UI, traductions i18n, helpers) sont directement liΓ©s Γ  la fonctionnalitΓ© demandΓ©e d'export de paroles vers fichier autonome.
Docstring Coverage βœ… Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lyrics-editor-export-standalone

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) type: feat New feature size: l 200-500 lines labels Jun 16, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/lyrics.rs`:
- Around line 1579-1583: The export_lyrics_to_path function writes user data to
disk without first validating that an active profile exists, which violates the
coding guideline that all commands touching user data must go through
state.require_profile_pool().await?. Add a call to
state.require_profile_pool().await? at the beginning of the
export_lyrics_to_path function (after the path creation) to ensure the active
profile is validated before proceeding with the file write operation. This
requires accepting a state parameter in the function signature as well.

In `@src/components/common/LyricsEditorModal.tsx`:
- Around line 1077-1083: The `defaultExportPath` function currently returns
`null` when `filePath` is absent, but it should use the `stem` parameter as a
fallback to provide a suggested filename. Instead of immediately returning
`null` when `!filePath`, construct and return a default path using the `stem`
and `ext` parameters to ensure the export dialog always has a pre-filled
filename suggestion, even when no trackFilePath exists.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77ca24ee-6852-4ca9-b1b5-f7867d0b9476

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 7b45fd1 and a03c5a5.

πŸ“’ Files selected for processing (22)
  • src-tauri/crates/app/src/commands/lyrics.rs
  • src-tauri/crates/app/src/lib.rs
  • src/components/common/LyricsEditorModal.tsx
  • src/components/layout/LyricsPanel.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/lyrics.ts

Comment thread src-tauri/crates/app/src/commands/lyrics.rs
Comment thread src/components/common/LyricsEditorModal.tsx
## 1 β€” export_lyrics_to_path missing active-profile sentry

CLAUDE.md cross-cutting rule: "every command that touches user
data goes through `state.require_profile_pool().await?`". The
command writes to the filesystem (not the per-profile DB) so
strict reading of the rule wouldn't require it β€” but adding
the gate provides defence in depth (no active profile β†’ editor
unreachable β†’ IPC payload to this command is malformed) and
aligns the signature with every sibling
`commands::lyrics::*` handler. Cheap to add, hard to argue
against.

- `export_lyrics_to_path` now takes `state: tauri::State<'_,
  AppState>` as its first param.
- Calls `state.require_profile_pool().await?` first thing and
  drops the pool β€” the command never touches SQLite.
- Doc-comment extended to explain the sentry's role + that
  the pool isn't actually read.

## 2 β€” `defaultExportPath` returned `null` on missing filePath

The Tauri save dialog was getting `defaultPath: undefined` when
the editor opened on a track with no resolved `file_path`,
leaving the filename field blank. Returning
`${stem}.${ext}` as the fallback gives the OS-native dialog a
filename to pre-fill β€” it interprets a name-only `defaultPath`
as "last-used directory + this name" on Windows / macOS /
Linux, which is the right UX when we don't have a song path
to anchor on.

- `defaultExportPath` return type tightened from `string | null`
  to `string` since it now always produces one.
- Caller drops the `?? undefined` guard at the dialog
  invocation since the value can't be null any more.

## Validation

- `cargo check --workspace --all-targets` clean
- `bun typecheck` + `bun lint` clean
@InstaZDLL InstaZDLL merged commit ca506f3 into main Jun 16, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the feat/lyrics-editor-export-standalone branch June 16, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add 1 more option to save a lyric file ,that was created by lyrics editor (stand alon .lrc / txt.)

1 participant