You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Songs — session-only pins („Fixează sus"):
- App-global PinStore (@observable, in-memory → clears on quit); pinned
songs float into a "Fixate" group on top of BOTH list and grid, pin
toggle in the context menu + hover toggle on grid cards, clear-all in
the group header. No SwiftData field — deliberately ephemeral.
Media — rebuilt as a presentable module (not a storage browser):
- Type tabs (Toate|Foto|Video|Audio) + rich grid: video thumbnails with
duration badges, audio artwork, search on LibraryManager.mediaLibraryQuery;
double-click/Enter presents. Old toolbar filter + MediaDetailPanel removed;
selection flows through LibraryManager.selectedMediaItem (no notifications).
- MediaPresenter = THE one present path (grid, panel, session runner);
fullscreen IMAGES now project (decoded inside the security scope into
LiveContent.mediaImage, fit/fill honored); video fixed to use the shared
VideoPlayerService (the old local instance never reached the output).
- Panel now mirrors Bible/Songs anatomy: PresentationPreviewCard with
pendingMedia, prev/next stepping over the grid's exact ordering,
StyleQuickSettings .media section (loop + fit/fill), PanelFooter format.
- MediaItem.durationSeconds (additive) probed async at import + backfilled.
Schedule — full sessions referencing real library content:
- ScheduleItem.payloadJSON stores stable refs (song = HistoryStore.songKey
+ optional arrangement, bible = translation + book/chapter/verse numbers,
media = id + name fallback); title/content stay display snapshots.
- SessionService: per-itemType resolver registry (extensible), append/create;
missing content resolves to a greyed ⚠ row that navigation skips.
- SessionRunner (app-global) is the ONLY presenter for schedule items:
slide-by-slide next/prev (songs expand at present time with current song
options), click-to-jump, one-shot Show; ScheduleView + SchedulePreviewPanel
rewritten around it (duplicated show logic deleted).
- „Adaugă la sesiune" right-click menu shared by Bible verses, songs and
media („Sesiune nouă…" creates a dated session instantly).
- Add-item sheet gains a real media picker; move up/down; start/stop.
Sessions import/export (.tpschedule):
- Flat versioned JSON (SessionArchive, format marker REQUIRED on import)
with a requiredMedia manifest; media re-links by id then name on import,
unresolved files listed to the operator. UTI com.robyrew.toppresenter.schedule.
Tabs: manual rename removed everywhere — auto-naming only, Schedule tab
titles itself from the selected session (name + date, unit-tested).
Tests: 142 pass (pins, media filter/stepping, session payload/resolution/
runner navigation, archive round-trip, tab naming). AGENTS.md updated.
- The editor is called **"Editor de Teme"** everywhere (sheet title, toolbar, menu, footer button)
182
194
- Editor tabs: Layout / Text / Fundal / **Tranziții** — NO output/hardware settings in the editor; screen/window-level/transition/disconnect live in Settings (⌘,) ▸ Proiecție (`ProjectionSettingsTab`) AND compactly in the right bar's **Ieșire** disclosure (StyleQuickSettings `.output`, beneath General). Themes describe the LOOK, Settings describe the DEVICE
183
195
-**Per-presenter options** (`ContentOptions` keyed "bible"/"song"/"text", theme-persisted, resilient decoding): text transform (none/upper/lower), uppercase reference/title. Applied at RENDER time via `pm.displayFields(main:reference:translation:subtitle:contentKey:)` — output uses the live content key, the preview card uses its panel's `formatHint`. Extend ContentOptions (with decodeIfPresent defaults) when a presenter needs a new option
184
-
- Media module output prefs (NOT theme): `videoLoopsByDefault`, `fullscreenVideoFillRaw` — live in Settings ▸ Proiecție ▸ Media
185
-
- Toolbar rules: per-view items are conditional on `appState.selectedSidebarItem`; the Media filter Picker binds `@AppStorage("mediaTypeFilter")` which MediaView reads (never write UserDefaults directly from toolbar bindings); Freeze sits next to Black/Clear in the presentation group
196
+
- Media module output prefs (NOT theme): `videoLoopsByDefault`, `fullscreenVideoFillRaw` — Settings ▸ Proiecție ▸ Media AND the Media panel's StyleQuickSettings `.media` section
197
+
- Toolbar rules: per-view items are conditional on `appState.selectedSidebarItem`; the media kind filter lives in MediaView's OWN header (segmented `@AppStorage("mediaTypeFilter")` — the old toolbar Picker was removed, one filter UI); Freeze sits next to Black/Clear in the presentation group
198
+
-**Tabs auto-name only (v10.4)** — the manual "Rename Tab" toolbar button/alert was REMOVED; `autoTabTitle` in MainControlView derives per module (bible: "(RO) EDC100 – ref", songs, media, schedule: session name + date via the testable `MainControlView.scheduleTabDetail(name:date:)`). Don't reintroduce `@SceneStorage("tabCustomName")`
199
+
-**Media is a PRESENTABLE module (v10.4)**: MediaView = type tabs (Toate|Foto|Video|Audio) + rich grid (video/audio `durationSeconds` badges, artwork thumbnails via async `MediaThumbnailFactory`) + search on `libraryManager.mediaLibraryQuery`; selection = `libraryManager.selectedMediaItem` (NO notifications). The panel mirrors Bible/Songs anatomy and steps prev/next through `MediaLibrary.filter(...)` — THE one ordering shared with the grid. ALL "present media" paths go through `MediaPresenter.present` (fullscreen image = `pm.showMedia(kind:"image")` decodes the NSImage inside the caller's security scope → `LiveContent.mediaImage`; video = shared `VideoPlayerService`; audio = plays only, never claims the output). New media kinds = a `MediaKind` case + classify rule + icon
200
+
- **Sessions (v10.4)**: `ScheduleItem.payloadJSON` (= `SessionItemPayload`, resilient Codable) stores STABLE refs — song `HistoryStore.songKey` (+ optional versionID/versionName), bible translation-abbrev + book/chapter/verse numbers, media id + name fallback; `title/content/subtitle` remain display SNAPSHOTS. Resolution via `SessionService.resolve` (registry of `SessionItemResolving` per itemType — extend there, don't grow a switch); misses → `.missing` (greyed row + ⚠, runner skips). `SessionRunner` (app-global @Observable) is THE ONLY presenter for schedule items — slide-by-slide next/prev (songs expand via `buildSongSlides` with the CURRENT song options at present time), jump-to-item, `presentOnce` for one-shots. „Adaugă la sesiune" = shared `AddToSessionMenu` fragment in the Bible verse / song / media context menus („Sesiune nouă…" creates instantly, no sheet). `.tpschedule` = FLAT JSON (`SessionArchive`, schemaVersion 1, format "TopPresenter Session" REQUIRED on import) + `requiredMedia` manifest; media re-links by id→name on import — media files are NOT embedded
186
201
-**Sidebar (v10.2)** = `SidebarItem.contentItems` (bible/songs/media/schedule/customSlides) in the top `List`, + a PINNED bottom group (`utilityItems` = `.history`, `.account` as selectable destinations, plus a **Settings** button via `@Environment(\.openSettings)`). `.history`→`HistoryView`, `.account`→`ProfileView` (local prefs, `@AppStorage` only — no login) route through `ContentAreaView`; both return `EmptyView` in `PreviewPanelView` and the preview column is HIDDEN for them in `MainControlView` (full-width). Any new switch over `SidebarItem` must handle `.history` + `.account`
187
202
-**Single output window**: locate it via `presentationWindows` (plural) and call `dedupePresentationWindows()` (closes extras) at the top of `showPresentationWindow`/`movePresentationWindow`/`positionOnScreen` + after the launch auto-open (guarded by `hasPresentationWindow`); the presentation `WindowGroup` is `.restorationBehavior(.disabled)`. This killed the "two overlapping outputs" (state-restoration + auto-open) bug — don't reintroduce an unguarded `openWindow(.presentation)`
188
203
- **Song verified flag + edit log (v10.2)**: `Song.verified` (Bool, round-trips through GOAT — `songDictV2` writes `"verified"`, `TopPresenterSongImporter` reads it), `Song.modifiedDate` (drives the Recente sort), `Song.editLogJSON`→`editLog: [SongEditEntry]` (coarse change log, INTERNAL — not exported). The song editor snapshots the song to GOAT on open (`ExportService.exportSongToTopPresenterJSON`); **Renunță** reverts via `ImportService.applyResult(_:to:modelContext:)` (the GOAT→Song builder extracted from `createSongFromResult` — clears + rebuilds versions/sections, reused by import too); **Gata** diffs old↔new via `ImportService.summarizeChanges(old:new:)` → appends edit-log entries. `SectionEditorCard` uses `@FocusState` so clicking a section drives the editor preview. Library: verified badge in `songBadges`, "Doar verificate" filter + `verificat`/✓ search token, sort header chips (`SongSortKey` = A-Z/Artist/Carte/Limbă/Recente). Song slide thumbnails have PREVIEW + trash (delete = remove the section behind the slide, `.confirmationDialog`)
@@ -342,8 +357,9 @@ Unsigned builds require users to right-click → Open, or run `xattr -cr TopPres
All TopPresenter exports embed this identifier so importers can reliably distinguish them from generic JSON.
362
+
All TopPresenter exports embed this identifier so importers can reliably distinguish them from generic JSON. Import MUST check the field is PRESENT (strict probe) — resilient decoders default it, which would accept foreign JSON. UTIs: `com.robyrew.toppresenter.theme` (package) + `com.robyrew.toppresenter.schedule` (public.json).
0 commit comments