No description
  • TypeScript 97.3%
  • JavaScript 0.9%
  • HTML 0.8%
  • CSS 0.8%
  • Shell 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jonas a9f6dca1d2 feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen
- Rename ScoreSync → MusicPDF inkl. IndexedDB-Migration alter Daten
- Darkmode (System/Hell/Dunkel) für Library + Editor
- Editor: Multi-Select via Shift-Klick + Lasso (Rechtsklick) + Strg+A,
  Bulk-Edit (Farbe / Löschen) für mehrere Markierungen
- Editor: Fit-to-Width beim PDF-Laden, kein Flash mehr
- Editor: Scroll-to-Cue beim Selektieren einer Markierung
- Viewer: Annotation-Tool (Stift / Radierer / Farben / Stärken) mit
  SVG-Polylines in normalisierten Seitenkoords; persistent in App,
  ephemeral im Tafel-Widget
- Viewer: Undo/Redo (Strg+Z / Strg+Shift+Z) für Annotationen
- Viewer: Doppelklick auf Markierung springt im Playback dorthin
- Viewer: Auto-Fokus mit Zoom beim Play-Klick
- Viewer: Fokus-Modus bleibt beim Zoomen aktiv, Cue als Anker
- Viewer: feinere Mausrad-Zoomstufen
- Fix: YouTube-Player-Subscription lief zu früh — Time/Duration kamen nicht
  in der UI an, Timeline blieb leer
2026-05-12 21:03:04 +00:00
public feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
src feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
.gitignore feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
index.html feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
package-lock.json feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
package.json feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
postcss.config.js feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
README.md feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
start.sh feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
tafel.html feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
tailwind.config.js feat: MusicPDF-Umbenennung, Darkmode, Annotationen, viele UX-Verbesserungen 2026-05-12 21:03:04 +00:00
tsconfig.app.json feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
tsconfig.json feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
tsconfig.node.json feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
vite.config.ts feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00
vite.config.widget.ts feat: initialer Stand ScoreSync 2026-05-12 17:07:41 +00:00

MusicPDF

A local-first web app for syncing sheet music PDFs with audio playback — either a local audio file or a YouTube video. Mark regions on a page, attach a time and (optionally) a short note, then play back: the matching region pulses in sync with the recording.

Everything lives in the browser: PDFs and audio files are stored as Blobs in IndexedDB, projects can be exported to a portable .zip, and there is no backend at all.

Stack

  • React + TypeScript + Vite
  • Tailwind CSS
  • pdfjs-dist for PDF rendering
  • YouTube IFrame Player API + native <audio> for playback
  • idb for IndexedDB
  • jszip for import/export
  • lucide-react for icons

Getting started

npm install
npm run dev      # start the dev server (http://localhost:5173)
npm run build    # produce a production build in dist/
npm run preview  # serve the production build locally

Open the app in your browser — there is nothing else to configure.

How a project is structured

Each project consists of:

  • A PDF (the score)
  • A media source: either a YouTube URL/ID or an audio file
  • A list of cues. Each cue has a timestamp, a target page, a highlight rectangle (stored in normalized 0–1 coordinates so it survives zooming), and an optional note.

ZIP format

Exportieren produces a .zip containing:

project.json    # title, mediaType, mediaSource, cues, schemaVersion: 1
score.pdf       # the PDF
audio.<ext>     # only present when mediaType === "audioFile"

Importieren reads the same shape back, validates project.json, and adds the project to the local library with a fresh ID.

Views

  • Bibliothek — grid of saved projects with thumbnails; create, open, import, export, delete.
  • Bearbeiten — two-pane editor. Drag a rectangle on the page to create a cue at the player's current time. Selected cues expose resize handles and a note field. Keyboard shortcuts:
    • Space — play / pause
    • [ / ] — nudge the selected cue's time by ±0.1 s
    • Delete / Backspace — remove the selected cue
  • Spielen — distraction-free playback. The active cue is determined by a binary search over the time-sorted cues; the PDF auto-pages to the cue and the highlight pulses while the note (if any) appears in a tooltip.

Source layout

src/
  components/     reusable UI (Modal, Toast, PdfPage, MediaPanel, NewProjectModal)
  media/          MediaPlayer interface + YouTube and HTML audio implementations
  pdf/            pdf.js worker setup, render + thumbnail helpers, useDoc hook
  storage/        idb wrapper, project CRUD
  util/           formatTime, binary search, file download helpers
  views/          LibraryView, EditorView, PlayerView
  types.ts        ProjectRecord, Cue, ProjectJsonV1
  App.tsx         top-level routing
  main.tsx        entry point

The media player abstraction (src/media/player.ts) exposes a single interface:

interface MediaPlayer {
  play(): Promise<void> | void;
  pause(): void;
  seek(t: number): void;
  getCurrentTime(): number;
  getDuration(): number;
  isPlaying(): boolean;
  onTimeUpdate(cb: (t: number) => void): () => void;
  onPlayStateChange(cb: (playing: boolean) => void): () => void;
  destroy(): void;
}

The editor and player views only depend on this interface — they don't care whether the backend is YouTube or a <audio> element.

Notes

  • Object URLs created for audio playback are revoked when the player is destroyed.
  • TypeScript strict mode is on (tsconfig.app.json).
  • localStorage is used only for the small last-route preference; everything project-related lives in IndexedDB.