refactor: backend-only transpose, remove client-side transpose logic
This commit is contained in:
@@ -94,9 +94,16 @@ export async function listSongs(q = "", sort = "date", order = "desc"): Promise<
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getSong(id: string, applyCapo = false): Promise<Song | null> {
|
||||
const url = applyCapo
|
||||
? `${API_BASE}/songs/${id}?apply_capo=true`
|
||||
export async function getSong(
|
||||
id: string,
|
||||
opts: { applyCapo?: boolean; transpose?: number } = {},
|
||||
): Promise<Song | null> {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.applyCapo) params.set("apply_capo", "true");
|
||||
if (opts.transpose && opts.transpose !== 0)
|
||||
params.set("transpose", String(opts.transpose));
|
||||
const url = params.size
|
||||
? `${API_BASE}/songs/${id}?${params}`
|
||||
: `${API_BASE}/songs/${id}`;
|
||||
const res = await fetch(url);
|
||||
if (res.status === 404) return null;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Song } from "./types";
|
||||
|
||||
const NOTES_SHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
||||
const NOTES_FLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
|
||||
|
||||
function transposeChord(chord: string, semitones: number): string {
|
||||
const match = chord.match(/^([A-G][#b]?)(.*)/);
|
||||
if (!match) return chord;
|
||||
const [, root, descriptor] = match;
|
||||
const idx = NOTES_SHARP.indexOf(root) !== -1
|
||||
? NOTES_SHARP.indexOf(root)
|
||||
: NOTES_FLAT.indexOf(root);
|
||||
if (idx === -1) return chord;
|
||||
const newIdx = ((idx + semitones) % 12 + 12) % 12;
|
||||
const notes = semitones >= 0 ? NOTES_SHARP : NOTES_FLAT;
|
||||
return notes[newIdx] + descriptor;
|
||||
}
|
||||
|
||||
export function transposeSong(song: Song, semitones: number): Song {
|
||||
if (semitones === 0) return song;
|
||||
return {
|
||||
...song,
|
||||
sections: song.sections.map((section) => ({
|
||||
...section,
|
||||
lines: section.lines.map((line) => ({
|
||||
...line,
|
||||
chords: line.chords.map((cp) => ({
|
||||
...cp,
|
||||
chord: transposeChord(cp.chord, semitones),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { EditSongSheet } from "~/components/edit-song-sheet";
|
||||
import { DeleteSongDialog } from "~/components/delete-song-dialog";
|
||||
import { SectionNav } from "~/components/section-nav";
|
||||
import { AutoScrollControls } from "~/components/auto-scroll-controls";
|
||||
import { transposeSong } from "~/lib/transpose";
|
||||
import { extractUniqueChords } from "~/lib/song-utils";
|
||||
import { getSong } from "~/lib/api";
|
||||
import { useAuth } from "~/lib/auth";
|
||||
@@ -82,23 +81,19 @@ export default function SongDetail() {
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
getSong(id)
|
||||
getSong(id, { transpose: initOffset })
|
||||
.then((s) => {
|
||||
setBaseSong(s);
|
||||
setDisplayedSong(s);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
}, [id]); // eslint-disable-line
|
||||
|
||||
useEffect(() => {
|
||||
if (applyCapo && baseSong?.meta.capo) {
|
||||
getSong(id, true).then((s) => {
|
||||
if (s) setDisplayedSong(s);
|
||||
});
|
||||
} else {
|
||||
setDisplayedSong(baseSong);
|
||||
}
|
||||
}, [applyCapo]); // eslint-disable-line
|
||||
getSong(id, { applyCapo, transpose: offset }).then((s) => {
|
||||
if (s) setDisplayedSong(s);
|
||||
});
|
||||
}, [id, applyCapo, offset]);
|
||||
|
||||
function handleOffsetChange(newOffset: number) {
|
||||
setOffset(newOffset);
|
||||
@@ -158,9 +153,8 @@ export default function SongDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
const displayed = transposeSong(displayedSong, offset);
|
||||
const uniqueChords = extractUniqueChords(displayed.sections);
|
||||
const sectionItems = displayed.sections.map((s, i) => ({
|
||||
const uniqueChords = extractUniqueChords(displayedSong.sections);
|
||||
const sectionItems = displayedSong.sections.map((s, i) => ({
|
||||
label: s.label,
|
||||
index: i,
|
||||
}));
|
||||
@@ -200,7 +194,7 @@ export default function SongDetail() {
|
||||
>
|
||||
<div className="max-w-lg mx-auto lg:max-w-none">
|
||||
<ChordChart
|
||||
sections={displayed.sections}
|
||||
sections={displayedSong.sections}
|
||||
fontSize={fontSize}
|
||||
onChordClick={handleChordClick}
|
||||
/>
|
||||
@@ -224,7 +218,6 @@ export default function SongDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom toolbar: section nav + auto-scroll */}
|
||||
<div className="lg:hidden border-t border-border bg-background flex items-center">
|
||||
<SectionNav sections={sectionItems} onJump={handleSectionJump} />
|
||||
<AutoScrollControls scrollRef={scrollRef} />
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct UpdateSongRequest {
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct GetSongQuery {
|
||||
pub apply_capo: Option<bool>,
|
||||
pub transpose: Option<i8>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
|
||||
@@ -155,6 +155,13 @@ pub async fn get_song(
|
||||
song
|
||||
};
|
||||
|
||||
let song = match params.transpose {
|
||||
Some(semitones) if semitones != 0 => {
|
||||
ChordTransposer.transpose_song(&song, semitones)
|
||||
}
|
||||
_ => song,
|
||||
};
|
||||
|
||||
Ok(Json(song))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user